Why is my state not updating when called in useEffect()? - reactjs

Have an annoying little bug in my application, which I am not sure is thanks to Redux states or something wrong with how I updated a internal state in React in a useEffect(). Either way I cannot seem to find an answer
I have a few console.log() throughout to try to better understand this.
I have a state in my App component, const [loading,setLoading] = useState(true) which determined where the page is loading, and if it is, returns a "loading" message on screen until all operations are completed. Inside my useEffect, depending on whether it is able to find a user in local storage, updates the state loading to false if there is a user found or if there isn't. When loading is false, the rest of applications routes are able to be rendered.
The issue I am having though, is that it seems as if my state does not initially update inside useEffect?
I do not have Strict mode because I am trying to see whats going on.
I have included a pic of my console logs, to give you an idea. As you can see after the useEffect is ran, even though it should updated loading state to false, upon rendering it has not updated, but somehow the dispatch inside useEffect has ran, because it shows the redux state has updated. Makes no sense to me.
After that, the App is loaded again, and somehow the loading state is not correct? Makes no sense to me
// React
import { useEffect, useState } from 'react'
// Redux
import { useSelector, useDispatch } from 'react-redux'
// React Reducer
import { Routes, Route } from "react-router-dom"
import { loginUser, logoutUser } from './reducers/user';
// Components
import Login from './components/Login';
import Player from './components/Player';
function App() {
console.log("app start")
const dispatch = useDispatch()
const user = useSelector(state => state.user)
console.log("user:",user)
const [loading,setLoading] = useState(true)
console.log("state:", loading)
useEffect(() => {
console.log("in app effet")
const loggedUserJSON = window.localStorage.getItem('loggedUser')
if (loggedUserJSON) {
console.log("inside json conditional")
const user= JSON.parse(loggedUserJSON)
dispatch(loginUser(user))
setLoading(false)
return
}
setLoading(false)
}, [])
// If loading is false
if (loading === false) {
return (
<div>
{user && <h1><button onClick={() => dispatch(logoutUser())}>LOG OUT</button></h1>}
<Routes>
<Route path="/" element={!user ? <Login/> : <h1>Home</h1>} />
<Route path="/player/:username" element={<Player/>}/>
{user && <Route path ='/store' element={<h1>STORE</h1>}/>}
</Routes>
</div>
)
}
console.log("end of app component")
// If Loading is true, not updated
return (
<h1>LOADING</h1>
);
}
export default App;

Try this, you may not get the correct value of loading just by console.log as batch process happens for the set state.
// React
import { useEffect, useState } from 'react'
// Redux
import { useSelector, useDispatch } from 'react-redux'
// React Reducer
import { Routes, Route } from "react-router-dom"
import { loginUser, logoutUser } from './reducers/user';
// Components
import Login from './components/Login';
import Player from './components/Player';
function App() {
console.log("app start")
const dispatch = useDispatch()
const {user} = useSelector(state => state.user)
console.log("user:",user)
const [loading,setLoading] = useState(true)
console.log("state:", loading)
useEffect(() => {
console.log("in app effet")
const loggedUserJSON = window.localStorage.getItem('loggedUser')
if (loggedUserJSON) {
console.log("inside json conditional")
const user= JSON.parse(loggedUserJSON)
dispatch(loginUser(user))
//setLoading(false)
}
setLoading(false)
}, [])
// If loading is false
if (!loading) {
return (
<div>
{user && <h1><button onClick={() => dispatch(logoutUser())}>LOG OUT</button></h1>}
<Routes>
<Route path="/" element={!user ? <Login/> : <h1>Home</h1>} />
<Route path="/player/:username" element={<Player/>}/>
{user && <Route path ='/store' element={<h1>STORE</h1>}/>}
</Routes>
</div>
)
}
else
return <h1>LOADING</h1>
// console.log("end of app component")
// If Loading is true, not updated
}
export default App;

Related

Reducer state update causing a router wrapped in HOC to rerender in a loop

I found that the issue is stemming from a Higher Order Component that wraps around a react-router-dom hook.
This Higher Order Component is imported from #auth0/auth0-react and is a requirement in our project to handle logging out with redirect.
However, even just a basic HOC, the issue is persisting.
in my App.js file, I have a react-redux provider. And inside the provider I have a ProtectLayout component.
ProtectLayout checks for an error reducer, and if the error property in the reducer has a value, it sets a toast message, as seen below.
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import PageLoader from "../loader/PageLoader";
import { useToast } from "../toast/ToastContext";
import { selectError } from "../../store/reducers/error/error.slice";
import ProtectedRoute from "../routes/ProtectedRoute";
const JobsPage = Loadable({
loader: () => import("../../screens/jobs/JobsPage"),
loading: () => <PageLoader loadingText="Getting your jobs..." />
});
const ProtectedLayout = () => {
const { openToast } = useToast();
const { error } = useSelector(selectError);
const getErrorDetails = async () => {
if (error) {
if (error?.title || error?.message)
return { title: error?.title, message: error?.message };
return {
title: "Error",
message: `Something went wrong. We couldn't complete this request`
};
}
return null;
};
useEffect(() => {
let isMounted = true;
getErrorDetails().then(
(e) =>
isMounted &&
(e?.title || e?.message) &&
openToast({ type: "error", title: e?.title, message: e?.message })
);
return () => {
isMounted = false;
};
}, [error]);
return (
<Switch>
<ProtectedRoute exact path="/" component={JobsPage} />
</Switch>
);
};
export default ProtectedLayout;
ProtectLayout returns another component ProtectedRoute. ProtectedRoute renders a react-router-dom Route component, which the component prop on the Route in the component prop passed into ProtectedRoute but wrapped in a Higher Order Component. In my actual application, as aforementioned, this is the withAuthenticationRequired HOC from #auth0/auth0-react which checks if an auth0 user is logged in, otherwise it logs the user out and redirects to the correct URL.
import React from "react";
import { Route } from "react-router-dom";
const withAuthenticationRequired = (Component, options) => {
return function WithAuthenticationRequired(props) {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, ...args }) => {
return <Route component={withAuthenticationRequired(component)} {...args} />;
};
export default ProtectedRoute;
However, in one of the Route components, JobsPage the error reducer state is updated on mount, so what happens is the state gets updated, the ProtectedLayout re-renders, which then re-renders ProtectedRoute, which then re-renders JobPage which triggers the useEffect again, which updates the state, so you end up in an infinite loop.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getGlobalError } from "../../store/reducers/error/error.thunk";
const JobsPage = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalError(new Error("test")));
}, []);
return (
<div>
JOBS PAGE
</div>
);
};
export default JobsPage;
I have no idea how to prevent this rendering loop?
Really all I want to do, is that when there is an error thrown in a thunk action, it catches the error and updates the error reducer state. That will then trigger a toast message, using the useToast hook. Perhaps there is a better way around this, that what I currently have setup?
I have a CodeSandbox below to recreate this issue. If you click on the text you can see the re-renders occur, if you comment out the useEffect hook, it will basically crash the sandbox, so might be best to only uncomment when you think you have resolved the issue.
Any help would be greatly appreciated!

React - Warning: Cannot update a component (`PrivateRoute`) while rendering a different component (`Modules`)

Getting the following error on all child components.
react-dom.development.js:86 Warning: Cannot update a component
(PrivateRoute) while rendering a different component (Example). To
locate the bad setState() call inside Examples,
I've found lots of examples of the same error but thus far no solutions
React Route Warning: Cannot update a component (`App`) while rendering a different component (`Context.Consumer`)
Can Redux cause the React warning "Cannot update a component while rendering a different component"
The PrivateRoute wraps the component to redirect if not logged in.
export default function PrivateRoute() {
const session: ISessionReducer = useSelector((state: RootState) => state.core.session);
useEffect(() => {
if (!session.jwt) <Navigate to="/login" />;
}, [session]);
return <Outlet />;
};
It is happening because useEffect runs after the component is rendered. So what's happening in this case is that your Outlet component is getting rendered first before your code in useEffect runs. So if the jwt token doesn't exist then it will try to redirect but it won't be able to because your Outlet will already be rendered by then.
So I can give you the solution of what I use to check if the jwt token exist.
1.) I create a custom hook for checking if the token exists.
2.) And then I use that custom hook in my privateRoute component to check if the user is loggedIn.
useAuthStatus.js
import { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
export const useAuthStatus = () => {
const [loggedIn, setLoggedIn] = useState(false)
const [checkingStatus, setCheckingStatus] = useState(true)
const { user } = useSelector((state) => state.auth)
useEffect(() => {
if (user?.token) {
setLoggedIn(true)
} else {
setLoggedIn(false)
}
setCheckingStatus(false)
}, [user?.token])
return { loggedIn, checkingStatus }
}
PrivateRoute component
import { Navigate, Outlet } from 'react-router-dom'
import { useAuthStatus } from '../../hooks/useAuthStatus'
import CircularProgress from '#mui/material/CircularProgress'
const PrivateRoute = () => {
const { loggedIn, checkingStatus } = useAuthStatus()
if (checkingStatus) {
return <CircularProgress className='app__modal-loader' />
}
return loggedIn ? <Outlet /> : <Navigate to='/login' />
}
export default PrivateRoute

Update site immediately after setting Local Storage

I am making a mern stack application and currently I am trying to switch between the login route and main page depending if you are logged in or not. However, this only works once I refresh the page, is there any way I can make it work without having to refresh the page?
App.js
{!localStorage.getItem('token') ? (
<Redirect exact from='/' to='/login' />
):
<>
<Navbar />
<Redirect to='/' />
</>
}
Reacting to changes in local storage is -at best- a weird approach. In practice, the only way for a component to re-render, is by the props that it receives to change, or by using component state via useState.
I'll write this imaginary piece of code to illustrate my point:
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
// ...
const LoginPage = _props {
const [token, setToken] = useState(localStorage.getItem('token'))
if (token) {
return <Redirect to='/' />
}
// I have no idea how you login your users
return (
<div>
<LoginForm onToken={setToken} />
</div>
)
}
If you need component A to react to changes done by component B, where neither of them is a direct child of the other, you will need global state.
Global state is similar to component state in that changes on it should trigger a re-render on the component that depends on it. But it is global, not local to a particular component.
To achieve this, there are complex solutions like redux, but you can implement a very simple version of it using a React Context:
// src/providers/GlobalStateProvider.js
import React, { createContext, useContext, useState } from 'react'
const Context = createContext()
const GlobalStateProvider = ({ children }) => {
const [token, doSetToken] = useState(localStorage.getItem('token'))
const setToken = t => {
doSetToken(t)
localStorage.setItem('token', token)
}
return (
<Context.Provider value={{ token, setToken }}>
{children}
</Context>
)
}
export { Context }
export default GlobalStateProvider
// src/App.js
import GlobalStateProvider from './providers/GlobalStateProvider'
// ...
const App = _props => {
return (
{/* Any component that is descendant of this one can access the context values, an will re-render if they change */}
<GlobalStateProvider>
{/* ... the rest of your components */}
</GlobalStateProvider>
)
}
// ...
// your particular component
import React, { useContext } from 'react'
import { Context } from 'path/to/GlobalStateProvider'
const SomeComponent = _props => {
// Component will re-render if token changes
// you can change token from wherever by using `setToken`
const { token, setToken } = useContext(Context)
if (token) {
// do this
} else {
// do that
}
}

trying to use cookies in my react app but it gets re-rendered all the time

I have a react app with sign in/out functionality. I want the application to keep logged in after reloading, and for that I'm using cookies.
The problem is that after I implemented what I thought should work, I get a "warning, maximum update depth exceeded. it can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render" message.
What am I doing wrong?
Thanks a lot!!
Here is the code.
sessions.ts
import React from "react";
import * as Cookies from "js-cookie";
export const setSessionCookie = (session: any): void => {
Cookies.remove("session");
Cookies.set("session", session, { expires: 14 });
};
export const getSessionCookie: any = () => {
const sessionCookie = Cookies.get("session");
if (sessionCookie === undefined) {
return {};
} else {
return JSON.parse(sessionCookie);
}
};
export const SessionContext = React.createContext(getSessionCookie());
main.tsx
import React, { useState, useEffect } from "react";
import { Switch, Route, Router } from "react-router-dom";
import Comp1 from "./components";
import Comp2 from "./components";
import { getSessionCookie, SessionContext } from "./session";
import { createBrowserHistory } from "history";
export default function Main() {
const [session, setSession] = useState(getSessionCookie());
useEffect(() => {
setSession(getSessionCookie());
}, [session]);
const history = createBrowserHistory();
return (
<SessionContext.Provider value={session}>
<Router history={history}>
<Switch>
<Route exact path="/" component={Comp1} />
<Route path="/comp2" component={Comp2} />
</Switch>
</Router>
</SessionContext.Provider>
);
}
I think the problem is that inside your useEffect you are changing the session, and then watching for session changes. You could probably compare the new cookie and only update the session if the cookie has changed -
useEffect(() => {
const cookie = getSessionCookie();
if (cookie !== session) setSession(cookie);
}, [session]);
Not sure exactly what your cookie looks like - you may have to do a more complex comparison than !==.
const [session, setSession] = useState(getSessionCookie());
You get session
useEffect(() => { ... }, [session]);
Session changed, effect callback called.
setSession(getSessionCookie());
We are setting a new session here (note that getSessionCookie always returns a new object). Component will re-render.
Back to the top.

Simple logout component throws "Cannot update a component from inside the function body of a different component"

This little Logout.jsx component logs-out the user...
import React from 'react';
import { Redirect } from 'react-router';
import { useDispatch } from 'react-redux';
import { userLogout } from '../redux/actions/authActions';
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
dispatch(userLogout());
return <Redirect to={to} />;
};
export default Logout;
and is used in path /logout thus:
<Switch>
...
<Route exact path="/logout" component={Logout} />
In the console it gives the dreaded (and apparently serious) message:
Cannot update a component from inside the function body of a different
component
Can someone spot why, and how to fix it?
Using react 16.13.0
I think it's just a logical mistake causing this error to pop up from another component (than Logout), try logging out once:
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(userLogout());
}, [dispatch]);
return <Redirect to={to} />;
};
You don't want to dispatch (or logout) on every component render

Resources