1

So I am creating a react Application (A create-react-app) and it is a multiple pages website. It has authentication, and I've used JWT Authentication in the backend. Now, I am conditionally giving the routes. I've used AuthProvider and useContext to check for authentication. The code from app.js that might help understand:

{/*IMPORT STATEMENTS*/}

function App() {
    const {
        isAuthenticated,
        type,
        login,
        newToken,
        setType,
        setRole,
        setUserId,
    } = useContext(authContext);

    const LStoken = localStorage.getItem('token');
    const LStype = localStorage.getItem('type');
    const LSrole = localStorage.getItem('role');
    const LSuserId = localStorage.getItem('userId');

    useEffect(() => {
        if (LStoken && LStype && LSuserId) {
            newToken(LStoken);
            setType(LStype);
            setRole(LSrole);
            setUserId(LSuserId);
            login();
        }
    }, [
        LStoken,
        LStype,
        newToken,
        setType,
        login,
        setRole,
        setUserId,
        LSuserId,
        LSrole,
    ]);

    return (
        <Routes>
            {!isAuthenticated && (
                <>
                    <Route path="/" element={<IndexScreen />} />
                    <Route path="/login" element={<LoginScreen />} />
                    <Route path="/register" element={<SignupScreen />} />
                    <Route
                        path="/teacherregister"
                        element={<TeacherSignupScreen />}
                    />
                    <Route path="/*" element={<Navigate to={'/'} />} />
                </>
            )}
            {isAuthenticated && type === 'student' && (
                <>
                    <Route path="/intro" element={<IntroScreen />} />
                    <Route path="/session" element={<SessionScreen />} />
                    <Route path="/:sessionId/roles" element={<RolesScreen />} />
                    <Route path="/history" element={<HistoryScreen />} />
                    <Route
                        path="/:sessionId/structure"
                        element={<ProblemStructureScreen />}
                    />
                    <Route
                        path="/:sessionId/details"
                        element={<DetailsScreen />}
                    />
                    <Route
                        path="/:sessionId/selectproblem"
                        element={<SelectProblemScreen />}
                    />
                    <Route
                        path="/:sessionId/test"
                        element={<SocketTestScreen />}
                    />
                    <Route
                        path="/:sessionId/problem/"
                        element={<ProblemMapScreen />}
                    />
                    <Route
                        path="/:sessionId/problem/notes"
                        element={<ScribblePadScreen />}
                    />
                    <Route path="/*" element={<Navigate to={'/intro'} />} />
                </>
            )}
            {isAuthenticated && type === 'teacher' && (
                <>
                    <Route path="/intro" element={<IntroScreen />} />
                    <Route
                        path="/teacherquestions"
                        element={<TeacherSelectProblemScreen />}
                    />
                    <Route path="/createquestion" element={<FormScreen />} />
                    <Route path="/*" element={<Navigate to={'/intro'} />} />
                </>
            )}

            <Route path="/*" element={<Navigate to={'/'} />} />
        </Routes>
    );
}
export default App;

In the index.js, I am wrapping it in the following

<React.StrictMode>
    <AuthProvider>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </AuthProvider>
</React.StrictMode>;

now when I'm on a conditional route, say '/:sessionId/details', and I reload the page, it automatically redirects me to the '/*' route of that block, i.e., takes me back to the '/intro' page. How do I stop that from happening??

How do I solve this?

One thing worth noting is that when the route is unconditional, it does not redirect on reload. even the login page doesn't redirect. So I reckon the authentication is causing the problem.

For reference, this is my AuthContext.js:

import React, { createContext, useState } from 'react';

const authContext = createContext({
    isAuthenticated: false,
    token: null,
    type: 'student',
    role: '',
    userId: '',
    newToken: () => {},
    login: () => {},
    logout: () => {},
    setType: () => {},
    setUserId: () => {},
    setRole: () => {},
    validSession: () => {},
});

export { authContext };

const AuthProvider = ({ children }) => {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [token, setToken] = useState(null);
    const [type, setType] = useState('student');
    const [role, setRole] = useState(null);
    const [userId, setUserId] = useState(null);
    const [sessionId, setSessionId] = useState(null);

    const login = () => {
        setIsAuthenticated(true);
    };

    const logout = () => {
        setIsAuthenticated(false);
        setToken(null);
    };

    const newToken = (newToken) => {
        setToken(newToken);
    };

    const validSession = (sessionId) => {
        setSessionId(sessionId);
    };

    return (
        <authContext.Provider
            value={{
                isAuthenticated,
                token,
                type,
                role,
                userId,
                login,
                logout,
                newToken,
                setType,
                setRole,
                setUserId,
            }}
        >
            {children}
        </authContext.Provider>
    );
};

export default AuthProvider;

I also tried using the HashRouter instead of the BrowserRouter thinking it might solve the problem, but it didn't.

  • The `isAuthenticated` state is initially false, so any time the page loads or the app remounts `isAuthenticated` will be false, none of the "protected" routes will be mounted to be matched, and the `} />` is rendered. Are you expecting to persist the auth state somewhere? Also, see my answer [here](/a/66289280/8690857) to get a sense for a more typical route protection implementation. – Drew Reese Jul 04 '23 at 05:57
  • Thought so. But then again, in the app.js, I'm importing the Authcontext and using the login() function to set the isAuthenticated to true. The third code snipped I have provided, the one with the useEffect hook, is present in the app.js file before it returns all the routes. It just takes the localstorage items and checks if the user is authenticated. So technically it should not redirect back, right? – Shreyansh Tiwari Jul 04 '23 at 06:41
  • No, it will still redirect since `isAuthenticated` is initially false on the initial render cycle prior to the `useEffect` hook being called. The ***initial*** state needs to be the correct state. Can you clarify where/how you are accessing localStorage, both for setting values, and for reading them? – Drew Reese Jul 04 '23 at 06:52
  • So, on login, I'm setting the values of token, type, and userId (in the localstorage). Also, I'm using the `const LStoken = localStorage.getItem('token')` for all three of them in app.js, before I return all the routes. Should I add the if condition outside the useEffect too to make it run before it returns the routes? – Shreyansh Tiwari Jul 04 '23 at 07:02
  • Oh, I see now. That third code snippet, where exactly is that code being used? It doesn't look like it's the `AuthProvider`. Where is that code called? Can you [edit] to include a more complete [mcve]? – Drew Reese Jul 04 '23 at 07:07
  • Just edited it. I should've kept the whole App() function together to avoid confusion. – Shreyansh Tiwari Jul 04 '23 at 07:13

1 Answers1

1

The isAuthenticated state is initially false, so any time the page loads or the app remounts isAuthenticated will be false, none of the "protected" routes will be mounted to be matched, and the <Route path="/*" element={<Navigate to={'/'} />} /> is rendered.

The isAuthenticated state should be persisted to localStorage and initialized from localStorage. Move the useEffect hook logic from App into the AuthProvider where the state initialization and persistence can work. Use lazy state initialization functions to provide the initial state values, and a useEffect hook to persist the state updates to localStorage.

Example:

const AuthProvider = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState(() => {
    return !!JSON.parse(localStorage.getItem("isAuthenticated"));
  });
  ... similar for other useState hooks ...

  useEffect(() => {
    localStorage.setItem("isAuthenticated", JSON.stringify(isAuthenticated));
  }, [isAuthenticated]);

  ... similar for other states

  ...

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        ...
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

All consuming components should read the context value:

function App() {
  const {
    isAuthenticated,
    type,
    ...
  } = useContext(authContext);

  return (
    <Routes>
      {!isAuthenticated && (
        <>
          <Route path="/" element={<IndexScreen />} />
          <Route path="/login" element={<LoginScreen />} />
          <Route path="/register" element={<SignupScreen />} />
          <Route
            path="/teacherregister"
            element={<TeacherSignupScreen />}
          />
          <Route path="/*" element={<Navigate to={'/'} />} />
        </>
      )}
      {isAuthenticated && type === 'student' && (
        <>
          <Route path="/intro" element={<IntroScreen />} />
          <Route path="/history" element={<HistoryScreen />} />
          <Route path="/session" element={<SessionScreen />} />
          <Route path="/:sessionId">
            <Route path="roles" element={<RolesScreen />} />
            <Route path="structure" element={<ProblemStructureScreen />} />
            <Route path="details" element={<DetailsScreen />} />
            <Route path="selectproblem" element={<SelectProblemScreen />} />
            <Route path="test" element={<SocketTestScreen />} />
            <Route path="problem/" element={<ProblemMapScreen />} />
            <Route path="problem/notes" element={<ScribblePadScreen />} />
          </Route>
          <Route path="/*" element={<Navigate to={'/intro'} />} />
        </>
      )}
      {isAuthenticated && type === 'teacher' && (
        <>
          <Route path="/intro" element={<IntroScreen />} />
          <Route
            path="/teacherquestions"
            element={<TeacherSelectProblemScreen />}
          />
          <Route path="/createquestion" element={<FormScreen />} />
          <Route path="/*" element={<Navigate to={'/intro'} />} />
        </>
      )}

      <Route path="/*" element={<Navigate to={'/'} />} />
    </Routes>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 1
    thanks a lot! I didn't really know about how (or why) to do lazy state initialization and persistance. Read a little about it and understood online, and now the code works just fine. – Shreyansh Tiwari Jul 04 '23 at 08:09