Gatsby initial rerender on client side bug - javascript

So, I have been investigating this bug for a while now and I think I got to the root cause of it. I have a navbar that changes if the user is logged in or not. On server render (I am using gatsby), this property is obviously always false meaning that I will get the guest navbar from the server on initial render.
I have the following hook for checking isLoggedIn:
import { useContext, useState, useEffect } from 'react';
import { UserContext } from '../contexts/UserContext';
import { isAuthenticated } from '../services/pilates-studio/localstorage';
const useIsLoggedIn = () => {
const { user } = useContext(UserContext);
const [isLoggedIn, setIsLoggedIn] = useState(isAuthenticated);
useEffect(() => {
console.log('we update isLoggedIn');
setIsLoggedIn(!!user || isAuthenticated);
}, [user]);
return isLoggedIn;
};
export default useIsLoggedIn;
isAuthenticated retrieves the access token from the localStorage, so as soon as we hydrate the application on the client, and the user is already logged in, isAuthenticated will result to true.
So the first state of isLoggedIn on the client side will be true. So we get the guest navbar form the server but since there is no state change ever, we are not rerendering the navbar even so technically the navbar state isLoggedIn has changed.
My navbar:
const NavBarContainer = () => {
const isLoggedIn = useIsLoggedIn();
return (
<NavBar logo={Logo} menu={NavMenu} isMenuOpen={isMenuOpen}>
<NavBarItem key="courses" label="Kurse" link={isLoggedIn ? '/courses' : '/courses/starter'} />
<NavBarItem key="appointment" label="Termine" link="/appointment" show={isLoggedIn} />
<NavBarItem key="trainers" label="Trainer" link="/trainers" />
<NavBarItem key="login" label="Login" link="/login" show={!isLoggedIn} floatRight />
<NavMenuButton key="menu" label="Menu" onClick={handleMenuClicked} icon={<UserIcon />} show={isLoggedIn} floatRight />
</NavBar>
);
};
export default NavBarContainer;
If I change the initial state of my hook to
const [isLoggedIn, setIsLoggedIn] = useState(false);
I get it to rerender but now I have a navbar that "blinks"/"flickers" on inital loading since it rerenders from guest to logged in navbar in a second. I would like to avoid that flickering.
I am quite new to SSR and gatsby. How would you solve the flickering? How can I have the inital state of isLoggedIn being set to true and still force a rerender?
Thanks for the support.

Related

How to go around a truthy statement here?

I was trying to implement a navbar functionality in my web app which should be switching the information rendered based on a boolean residing in my redux status. Now, when the user is not logged in, the object fetched using my used selector is an empty one which means that is a truthy value hence not enabling me to toggle the element on the navabr as i wish. Is there a way to do that without modifying my redux status ?
Thank you in advance.
import './App.css';
import { Route, Routes, Link, Redirect, Navigate } from "react-router-dom";
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import SearchBar from "./components/searchBar";
import Main from './components/main';
import Login from './components/login';
import Register from './components/register';
let linkStyle = { textDecoration: "none", color: "white" };
function App() {
// fetching redux status
let user = useSelector(state => state.loginStatus.user)
let loginStatus = useSelector(state => state.loginStatus.isLoggedIn)
console.log(loginStatus)
const [isActive, setIsActive] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [avatar, setAvatar] = useState({});
useEffect(() => {
if (loginStatus) {
setIsLoggedIn(loginStatus)
setAvatar(user)
}
}, [loginStatus, isLoggedIn, isActive, avatar])
return (
<div className="App">
<nav className="navbar">
<div className="logo"><Link to={'/'} onClick={() => setIsActive(false)}><img src='https://cdn-icons-png.flaticon.com/512/201/201623.png' /></Link></div>
<h1 className="title">Travel.com</h1>
{
isLoggedIn && (<ul className='menu'>
<li className="link"><Link style={linkStyle} to='/login' onClick={() => setIsActive(true)}>Log-in</Link></li>
<li className="link"><Link style={linkStyle} to='/register' onClick={() => setIsActive(true)}>Register</Link></li>
</ul>)
}
{ /*
isLoggedIn && (
<div className="avatar">
<h6> Hi avatar.user_name!</h6>
</div>
) */
}
</nav>
If I understand your question correctly then a simple solution could be to check if the object is empty using something like: Boolean(Object.keys(logInStatus).length)
this will return false if logInStatus is an empty object, and true if it has any properties. You would pass this as the argument into setIsLoggedIn() in your useEffect().
You said you don't want to change the Redux store, however I would call into question the naming of the isLoggedIn property. By starting a property with is the name implies that it is a boolean value, or at the very least that it will coerse into a boolean value which corresponds to its name. This is something to watch out for as someone who doesn't know the codebase well would most likely end up making an assumption based on conventions, resulting in a situation like this with code behaving the opposite way to how you would expect.

What exactly does useContext do behind the scenes? [duplicate]

This question already has an answer here:
How to solve problem with too many re-renders in context?
(1 answer)
Closed 10 months ago.
Hi everyone I'm new to React. I was trying to create a global state using the context api. I encountered something unexpected. What I've learned is that when a context is created, it has a provider that will wrap around those components that need the data and when the value changes, the provider will rerender all the wrapped components, right?
Take a look at this:
// AuthContext.js
export const AuthContext = createContext(null);
const AuthProvider = ({ children }) => {
const [user, setUser] = useState({
name: null,
});
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
};
// index.js
<AuthProvider>
<App />
</AuthProvider>
// App.js
function App() {
console.log("[App] ran");
return (
<>
<Dashboard />
<Login />
</>
);
}
// Dashboard.js
function Dashboard() {
console.log("[Dashboard] ran");
return <div>Dashboard</div>;
}
// Login.js
function Login() {
console.log("[Login] ran");
const { user, setUser } = useContext(AuthContext);
const inputNameRef = useRef();
return (
<div>
<input placeholder="Enter your name..." ref={inputNameRef} />
<button
onClick={() => {
setUser(inputNameRef.current.value);
}}
>
Submit
</button>
</div>
);
}
When the code runs for the first time the output is:
[App] ran
[Dashboard] ran
[Login] ran
and when the submit button is clicked, the setUser function will be called and a new value will be set to the AuthProvider state. The provider should rerender all of the components, and the above output should again be logged but nope, the output is just:
[Login] ran
Something that is interesting to me is that when I use useContext in the Dashboard component it works, I mean the component will be rerendered. It's related to the useContext hook I think but don't know how it works. What is going on under the hood?
To quote React's official documentation on Context (https://reactjs.org/docs/context.html):
All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.
You can think of "consumer" as the component whose state "consumes" the useContext hook. AKA you'll only see a re-render from the components where the hook is used. This way not ALL children inside a Context Provider will be re-rendered on a state change, only those who consume it.

Using client-only routes with page templates coming from Contentful

Goal
I am looking to use client-only routes for content under a certain URL (/dashboard). Some of this content will be coming from Contentful and using a page template. An example of this route would be {MYDOMAIN}/dashboard/{SLUG_FROM_CONTENTFUL}. The purpose of this is to ensure projects I have worked on at an agency are not able to be crawled/accessed and are only visible to 'employers' once logged in.
What I have tried
My pages are generated via gatsby-node.js. The way of adding authentication/client-only routes has been taken from this example. Now the basics of it have been setup and working fine, from what I can tell. But the private routes seem to only work in the following cases:
If I'm logged in and navigate to /dashboard
I'm shown Profile.js
If I an not logged in and go to /dashboard
I'm shown Login.js
So that all seems to be fine. The issue comes about when I go to /dashboard/url-from-contentful and I am not logged in. I am served the page instead of being sent to /dashboard/login.
exports.createPages = async ({graphql, actions}) => {
const { createPage } = actions;
const { data } = await graphql(`
query {
agency: allContentfulAgency {
edges {
node {
slug
}
}
}
}
`);
data.agency.edges.forEach(({ node }) => {
createPage({
path: `dashboard/${node.slug}`,
component: path.resolve("src/templates/agency-template.js"),
context: {
slug: node.slug,
},
});
});
}
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions;
if(page.path.match(/^\/dashboard/)) {
page.matchPath = "/dashboard/*";
createPage(page);
}
};
My auth.js is setup (the username and password are basic as I am still only developing this locally):
export const isBrowser = () => typeof window !== "undefined";
export const getUser = () =>
isBrowser() && window.localStorage.getItem("gatsbyUser")
? JSON.parse(window.localStorage.getItem("gatsbyUser"))
: {};
const setUser = (user) =>
window.localStorage.setItem("gatsbyUser", JSON.stringify(user));
export const handleLogin = ({ username, password }) => {
if (username === `john` && password === `pass`) {
return setUser({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
});
}
return false;
};
export const isLoggedIn = () => {
const user = getUser();
return !!user.username;
};
export const logout = (callback) => {
setUser({});
call
};
PrivateRoute.js is setup the following way:
import React from "react";
import { navigate } from "gatsby";
import { isLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/dashboard/login`) {
navigate("/dashboard/login");
return null;
}
return <Component {...rest} />;
};
export default PrivateRoute;
dashboard.js has the following. The line <PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />, I have tried a couple of things here - Statically typing the route and using the exact prop, using route parameters such as /:id, /:path, /:slug :
import React from "react";
import { Router } from "#reach/router";
import Layout from "../components/Layout";
import Profile from "../components/Profile";
import Login from "../components/Login";
import PrivateRoute from "../components/PrivateRoute";
import Agency from "../templates/agency-template";
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/dashboard/url-from-contentful" component={Agency} />
<PrivateRoute path="/dashboard/profile" component={Profile} />
<PrivateRoute path="/dashboard" />
<Login path="/dashboard/login" />
</Router>
</Layout>
);
export default App;
And finally agency-template.js
import React from "react";
import { graphql, Link } from "gatsby";
import styled from "styled-components";
import SEO from "../components/SEO";
import Layout from "../components/Layout";
import Gallery from "../components/Gallery";
import GeneralContent from "../components/GeneralContent/GeneralContent";
const agencyTemplate = ({ data }) => {
const {
name,
excerpt,
richDescription,
richDescription: { raw },
images,
technology,
website,
} = data.agency;
const [mainImage, ...projectImages] = images;
return (
<>
<SEO title={name} description={excerpt} />
<Layout>
<div className="container__body">
<GeneralContent title={name} />
<Gallery mainImage={mainImage} />
<GeneralContent title="Project Details" content={richDescription} />
<div className="standard__images">
<Gallery projectImages={projectImages} />
</div>
<ViewWebsite>
<Link className="btn" to={website}>
View the website
</Link>
</ViewWebsite>
</div>
</Layout>
</>
);
};
export const query = graphql`
query ($slug: String!) {
agency: contentfulAgency(slug: { eq: $slug }) {
name
excerpt
technology
website
images {
description
gatsbyImageData(
layout: FULL_WIDTH
placeholder: TRACED_SVG
formats: [AUTO, WEBP]
quality: 90
)
}
richDescription {
raw
}
}
}
`;
export default agencyTemplate;
I assume that gating content from a CMS is possible with Gatsby but I might be wrong given it is an SSG. I may be misunderstanding the fundamentals of client-only. The concepts in React and using Gatsby are still very new to me so any help or guidance in achieving the goal would be appreciated.
What I ended up doing
So the answer I marked was the one that 'got the ball rolling'. The explanation of what was happening with state and requiring either useContext or redux helped me understand where I was going wrong.
Also, the suggestion to use web tokens prompted me to find more information on using Auth0 with the application.
Once I had got out of the mindset of creating pages using Gatsby (Through a template, via gatsby-node.s), and instead doing it in a 'React way' (I know Gatsby is built with React) by handling the routing and GraphQL it became clearer. Along with the authentication, all I ended up doing was creating a new <Agency /> component and feeding the data from GraphQL into it and updating the path with my map().
return (
<>
<Router>
<DashboardArea path="/dashboard/" user={user} />
{agencyData.map(({ node }, index) =>
node.slug ? (
<Agency key={index} data={node} path={`/dashboard/${node.slug}`} />
) : null
)}
</Router>
</>
);
I assume that in your PrivateRoute component, you're using the isLoggedIn check incorrectly. importing and using isLoggedIn from auth.js will run only initially and will not act as a listner. What you can do is that store the value of isLoggedin in global state variable like(useContext or redux) and make a custom hook to check for the login state. Secondly avoid accessing localStorage directly, instead use the global state managment (useContext, redux) or local state managment (useState, this.state).
Note: that when ever you go to a route by directly pasting url in browser, it always refreshes the page and all your stored state is reinitialized. This may be the reason why you may be experiencing this issue. The browser does not know that you had been previously logged in and therefore it always validates once your application is mounted. What you can do is that you can store isLoggedIn state in browser's localstore. Personally I like to use redux-persist for that.
export const useGetUser = () => { //add use infront to make a custom hook
return useSelector(state => state.gatsByUser) // access user info from redux store
};
export const handleLogin = ({ username, password }) => {
//suggestion: don't validate password on client side or simply don't use password,
//instead use tokens for validation on client side
if (username === `john` && password === `pass`) {
dispatch(setUserInfo({
username: `john`,
name: `Johnny`,
email: `johnny#example.org`,
isLoggedIn: true,
}));
return true;
}
return false;
};
// adding 'use' infront to make it a custom hook
export const useIsLoggedIn = () => {
//this will act as a listner when ever the state changes
return useSelector(state => state.gatsByUser?.isLoggedIn ?? false);
};
export const logout = (callback) => {
const dispatch = useDispatch(); // redux
dispatch(clearUserInfo());
};
Now in private route do
import React from "react";
import { navigate } from "gatsby";
import { useIsLoggedIn } from "../services/auth";
const PrivateRoute = ({ component: Component, location, ...rest }) => {
const isLoggedIn = useIsLoggedIn();
if (!isLoggedIn) {
return navigate("/dashboard/login");
}
return <Component {...rest} />;
};
export default PrivateRoute;
It looks like you're server-side rendering dashboard/[url] in gatsby-node.js/createPages()? IIRC those routes will have higher precedence than dynamic routes (which you specify with #reach/router in dashboard.js).
Plus, the content of those routes are currently publicly available. If you want to keep them truly private, you should query Contentful graphql API directly on the client side (via fetch() or use apollo client, urql, etc.), instead of relying on Gatsby's graphql server.
I would do the follows:
Removing the dashboard/[url] portion in your gatsby-node.js
Configure your web host so that all routes matches '/dashboard/*' will redirect to '/dashboard'
If you happen to host your static site on Netlify, you'd create a _redirects with this, assuming you configure Gatsby to create nice url:
# /static/_redirect
/dashboard/* /dashboard 200
A possible simpler way that match your current setup is gating content at web host level. You can configure nginx to protect /dasboard/* with basic auth. However maintaining/updating password is a pain & modern hosting solution don't really allow user to configure that.
Netlify offers its own authentication solution that you could look into.
I've had the same issue earlier and I couldn't get exact functionality with Private Routes.
In my case, I created two separate Layouts for Public and Private Routes and built the authentication to Private Layout. Logged-in user data were linked to a redux store (First I used Context, then moved to Redux). In Private routes with the Private Layout, it redirected the guest users to the Login page and redirected them to the same page after login.
Private layout is something like this:
import React from "react";
import { navigate } from "gatsby";
import { useSelector } from "react-redux";
const PrivateLayout = ({children}) => {
const isLoggedIn = useSelector(state => state.user.isLoggedIn);
useEffect(() => {
if (!isLoggedIn) {
// redirect the user to login page.
// I'm sending the current page's URL as the redirect URL
// so that I can take the user back to this page after logging in.
}
}, [isLoggedIn])
if (!isLoggedIn) return null;
return <>
{...header}
{children}
{...footer}
</>
}
export default PrivateLayout;
Not sure if this workaround suits you. If it does, I can give you more info.

Make conditional react component rerender after boolean changes

I have a react component that conditionally renders JSX according to the user's login state.
<div>
{ boolIsLoggedIn ?
<SomeLoggedInComponent /> : <SomeNotLoggedInComponent /> }
</div>
I think I need to use React.useState() and/or React.useEffect() but I'm not sure exactly how to implement it.
I've tried this:
const [boolIsLoggedIn, setBoolIsLoggedIn] = useState(isLoggedIn())
useEffect(() => {
const checkLogIn = () => {
setBoolIsLoggedIn(isLoggedIn())
}
checkLogIn()
})
Where isLoggedIn() checks whether the user is logged in or not and returns a boolean.
Currently, I can log out and log in and the isLoggedIn() function works, but the component I want to conditionally re-render doesn't do so until I refresh the page.
So I added [isLoggedin()] as the second parameter to useEffect() and now it almost works. When I log in, the boolIsLoggedIn value changes to true and the component re-renders. However, when I log back out boolIsLoggedIn doesn't change until I refresh the page.
Here is my code for the isLoggedIn() function, which is coded in a seperate file and imported:
let boolIsLoggedIn = false
export const setUser = user => {
//handle login
boolIsLoggedIn = true
export const logout => {
//handle logout
boolIsLoggedIn = false
}
export const isLoggedIn = () =>
return boolIsLoggedIn
}
try this
import React, { Component } from "react";
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: isLoggedIn()
};
}
render() {
let { isLoggedIn } = this.state;
return (
<div className="App">
{(function() {
if (isLoggedIn) {
return <div>Login</div>;
} else {
return <div>with out Login</div>;
}
})()}
</div>
);
}
}
export default App;
You are missing the open [ on defining the state.
I would set the defailt value on false so you won't execute the function twice.
The use effect usually needs a dependencies array as the second parameter but as you only want to run it once (after the component is mounted) just place an empty array as the second parameter.
Another thing is that if the isLoggedIn function is asyncronous you have to wait for it by using await and setting the parent function as async. This would definetly be the problem you have if the function is asyncronous.
Change This:
const boolIsLoggedIn, setBoolIsLoggedIn] = useState(isLoggedIn())
useEffect(() => {
const checkLogIn = () => {
setBoolIsLoggedIn(isLoggedIn())
}
checkLogIn()
})
To this:
const [boolIsLoggedIn, setBoolIsLoggedIn] = useState(isLoggedIn());
useEffect(() => {
(() => {
setBoolIsLoggedIn(isLoggedIn());
})();
}, [isLoggedIn()]);
Good Luck
You need to be able to share react state, and not just values in order to trigger a react re-render. In your example, you would need a call to setBoolIsLoggedIn() whenever value of isLoggedIn() changes in order to change the state and trigger a re-render of the component.
Another way share state between components is through React Context. You would first need to create a Context Provider like this:
const UserContext = React.createContext();
export function UserProvider({ children }) {
const [ user, setUser ] = useState({ loggedIn: false });
return (
<UserContext.Provider value={[ user, setUser ]}>
{ children }
</UserContext.Provider>
)
}
In this case, the UserProvider is the one maintaining the shared state withuser={ loggedIn }. You would then need to wrap your React App component with this provider in order for components to be able to share state.
To set the shared user state you can use the hook useContext(UserContext) and change state through setUser provided by the hook. In the example below, we have a login button component that sets shared user state:
function LoginButton() {
const [user, setUser] = useContext(UserContext);
/** flip login state when button is clicked */
function logIn() {
setUser(state => ({ ...state, loggedIn: !state.loggedIn }) );
}
return (
<button onClick={logIn}>{ user.loggedIn ? "Log Out" : "Log In"}</button>
);
}
And finally, to be able to use the shared user state and trigger a re-render whenever the user state changes, you would again use the hook useContext(UserContext) in a different component to be able to get the state like this:
export default function App() {
const [user,] = useContext(UserContext);
return (
<div className="App">
<h1>Conditional React component</h1>
<h2>Click on the button to login/logout</h2>
<LoginButton></LoginButton>
<div>
{ user.loggedIn ? <LoggedInComponent/> : <LoggedOutComponent/>}
</div>
</div>
);
}
I've provided an example here to show how this all works together.

How to print another component when i logout. React and Firebase

How I can render another component when i logout on firebase.
I´m trying to re-render the page to print the LoginPage
This is my LoginPage that is render when I loggin with another form.
import React, { Component } from "react";
/*Importing firebase*/
import firebase from "firebase";
/*Importing pages*/
import LoginPage from "../login/LoginPage";
/*Importing components*/
import Header from "./containers/HandleHeader";
class IndexPage extends Component {
shouldComponentUpdate() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
} else {
this.forceUpdate();
return <LoginPage />;
}
});
}
render() {
return (
<div>
<Header />
</div>
);
}
}
export default IndexPage;
And this is my handleLogout that work when I click my logout button.
handleLogout = e => {
firebase
.auth()
.signOut()
.then(() => this.forceUpdate());
};
I want to make that when I logout I don´t need reload the page.
Usually the best way to do this is to maintain the logged-in state somewhere, then protect the entry points to any components that require authentication with logic like this:
render() {
const { loggedIn } = this.props;
if (!loggedIn) return <Redirect to="/login" />;
// Reset of component rendered below this point
}
Note that this logic can be in the component itself, some parent component, or some higher order component. The key is to have it somewhere that will prevent access to any protected component by redirecting in the render method before any protected information can be reached.
Redirecting is often achieved using some routing package like, say, react-router-dom, to navigate around. This means that when you log out, a user is implicitly always redirected because they can no longer access the protected components anymore.

Categories

Resources