Home > Enterprise >  React state not changing in component
React state not changing in component

Time:10-05

I'm trying to create protected routes that are only viable while user is logged in, but I have trouble getting loggedIn state in ProtectedRoutes component, it's always set to false thus redirecting to "/login". What am I not getting correctly here?

App.tsx

interface loginContextInterface {
    loggedIn: boolean;
    setLoggedIn: (value: (((prevState: boolean) => boolean) | boolean)) => void;
}

export const LoginContext = createContext({} as loginContextInterface);

export default function App() {
    const [ loggedIn, setLoggedIn ] = useState(false)

     useEffect(() => {
        console.log("before", loggedIn)
        isLoggedIn().then((r) => {
            console.log("R", r)
            setLoggedIn(r)
        })
        console.log("after", loggedIn)
    }, [loggedIn])

    return (
        <LoginContext.Provider value={{loggedIn, setLoggedIn}}>
            <Router>
                <MenuHeader />
                <Routes>
                    <Route path="/" element={<Home/>}/>
                    <Route path="/tasks" element={<ProtectedRoutes/>}>
                        <Route path="/" element={<Tasks/>}/>
                    </Route>
                    <Route path="/login" element={<Login />}/>
                    <Route path="/logout" element={<Logout />}/>
                    <Route path="/register" element={<Register/>}/>
                </Routes>
            </Router>
        </LoginContext.Provider>
  );
}

ProtectedRoutes.tsx


export const ProtectedRoutes = () =>{
    const location = useLocation();
    const {loggedIn} = useContext(LoginContext)

    console.log("protected", loggedIn)

    return (
        loggedIn ? <Outlet/> : <Navigate to={"/login"} replace state={{location}}/>
    );
}

Edit:

isLoggedIn just authenticates that the user is logged in via cookie using api on the server side. Added logging

Produces these after trying to access /tasks route and redirecting me to /login again

VM109:236 protected false
App.tsx:21 before false
App.tsx:26 after false
App.tsx:21 before false
App.tsx:26 after false
2App.tsx:23 R true
App.tsx:21 before true
App.tsx:26 after true
App.tsx:23 R true

CodePudding user response:

It's not recommended to reset the dependency inside the useEffect(), it may cause an infinite loop.

useEffect(() => {
isLoggedIn().then((r) => setLoggedIn(r)) // loggedIn will be update here and trigger 
                                            the useEffect agan
 }, [loggedIn])

What does the console.log(loggedIn) and console.log(r) show? I'm guessing isLoggedIn returns false, loggedIn is set to false initially so useEffect not being triggered again and it remains as false

CodePudding user response:

There is an issue with the useEffect hook, using the loggedIn state value as the dependency. You should not use dependencies that are unconditionally updated by the hook callback. My guess here is that you wanted to do an initial authentication check when the app mounts. You can remove loggedIn from the dependency since it's not referenced at all.

useEffect(() => {
  isLoggedIn().then(setLoggedIn);
}, []);

I suggest also using an initial loggedIn state value that doesn't match either the authenticated or unauthenticated states, i.e. something other than true|false. This is so the ProtectedRoutes can conditionally render null or some loading indicator while any pending authentication checks are in-flight and there isn't any confirmed authentication state already saved in state.

Update the context to declare loggedIn optional.

interface loginContextInterface {
  loggedIn?: boolean;
  setLoggedIn: React.Dispatch<boolean>;
}

Update App to have an initially undefined loggedIn state value.

const [loggedIn, setLoggedIn] = useState<boolean | undefined>();

Update ProtectedRoutes to check for the undefined loggedIn state to render a loading indicator and not immediately bounce the user to the login route.

const ProtectedRoutes = () => {
  const location = useLocation();
  const { loggedIn } = useContext(LoginContext);

  if (loggedIn === undefined) {
    return <>Checking authentication...</>; // example
  }

  return loggedIn ? (
    <Outlet />
  ) : (
    <Navigate to={"/login"} replace state={{ location }} />
  );
};

Also in App, remove/move the "/tasks" path from the layout route rendering the ProtectedRoutes component to the nested route rendering the Tasks component. The reason is that it's invalid to nest the absolute path "/" in "/tasks".

<Route element={<ProtectedRoutes />}>
  <Route path="/tasks" element={<Tasks />} />
</Route>

Edit react-state-not-changing-in-component

  • Related