Search code examples
reactjsreact-hooksreact-routeruse-effect

Code not running inside a React component


I'm creating a React app and I'm trying to protect some routes behind password authorization.

My component to handle this is the following :

(ProtectedRoute.js)

export default function ProtectedRoute({ code, redirect_path = '/login' }) {

    if (code === 401) return <Navigate to={redirect_path} replace/>;
    return <Outlet/>;

}

All I need to do is pass the HTTP status code returned by my back-end and if it corresponds to an unauthorized response then it asks the user to log in, otherwise they are allowed through.

I query my back-end from App.js in the following way :

(App.js)

export default function App() {

    let [status, setStatus] = useState(401);
    useEffect(() => {
        fetch(
            'http://localhost:5000/auth',
            {
                method: 'GET',
                mode: 'cors',
                headers: {
                    'id_token': sessionStorage.getItem('id_token')
                }
            }
        )
            .then(response => response.status)
            .then(status => setStatus(status));
    }, []);
    console.log('status=' + status);

    return (
        <div className="App">
            <Navbar is_logged_in={status === 200}/>
            <ToastContainer/>
            <Routes>
                <Route index element={<Home/>}/>
                <Route path="/vehicle/:id" element={<EV/>}/>
                <Route element={<ProtectedRoute code={status}/>}>
                    <Route path="/add" element={<AddEV/>}/>
                </Route>
                <Route path="/filter" element={<Home/>}/>
                <Route path="/compare" element={<Home/>}/>
                <Route path="/login" element={<LoginRegister type="login"/>}/>
                <Route path="/register" element={<LoginRegister type="register"/>}/>
                <Route path="/logout" element={<Logout/>}/>
                <Route path="*" element={<p>Nothing here: 404!</p>}/>
            </Routes>
        </div>
    );
}

This works, except for one problem. When the user is sent to the login page and successfully authenticates, they are then sent back to the index. At this point, I would expect the code inside the useEffect hook to fire and update the value of status.

Instead nothing happens. If I manually refresh the page then the code is triggered and everything happens the way I'd expect it to.

Clearly the idea is correct but the implementation is wrong. Is what I want to achieve possible ? If so how should I go about it ?

Thanking you in advance for your suggestions.


Solution

  • The useEffect has an empty dependency, so it runs only once when the App component mounts. This is why it "works" when you reload the page. It reloads the app, i.e. remounts App component and the useEffect hook callback is invoked.

    It is common to abstract "auth" state into a React Context, to be accessed by the app where necessary, like when attempting to access protected routes or logging in/out.

    Example:

    import { createContext, useContext, useState } from 'react';
    
    const AuthContext = createContext({
      checkAuthStatus: () => {},
      status: 401,
      setStatus: () => {},
    });
    
    const useAuth = () => useContext(AuthContext);
    
    const AuthProvider = ({ children }) => {
      const [status, setStatus] = useState(401);
    
      const checkAuthStatus = () => {
        fetch(
          'http://localhost:5000/auth',
          {
            method: 'GET',
            mode: 'cors',
            headers: {
              'id_token': sessionStorage.getItem('id_token')
            }
          }
        )
          .then(response => response.status)
          .then(status => setStatus(status));
      };
    
      return (
        <AuthContext.Provider value={{ checkAuthStatus, status, setStatus }}>
          {children}
        </AuthContext.Provider>
      );
    };
    
    export default AuthProvider;
    export {
      AuthContext,
      useAuth
    };
    

    ...

    Wrap the App component.

    import AuthProvider from "../path/to/AuthProvider";
    
    ...
    
    <AuthProvider>
      <App />
    </AuthProvider>
    

    Use the useAuth hook to access the status and state updater function.

    export default function ProtectedRoute({ redirect_path = '/login' }) {
      const { status } = useAuth();
    
      return status === 401
        ? <Navigate to={redirect_path} replace />
        : <Outlet/>;
    }
    

    ...

    const Navbar = () => {
      const { status } = useAuth();
      const is_logged_in= status === 200;
    
      ...
    
      return ....;
    };
    

    Login/Logout

    const Login = ({ type }) => {
      const { setStatus } = useAuth();
    
      const loginHandler = (....) => {
        ...
        setStatus(200);
      };
    
      ...
    
      return ....;
    };
    
    const Logout = () => {
      const { setStatus } = useAuth();
    
      const logoutHandler = (....) => {
        ...
        setStatus(401);
      };
    
      ...
    
      return ....;
    };
    

    Use the useEffect hook with a dependency on the current location's pathname value to issue the fetch when the path is "/".

    export default function App() {
      const { pathname } = useLocation();
      const { checkAuthStatus, status } = useAuth();
    
      useEffect(() => {
        if (pathname === "/") {
          checkAuthStatus();
        }
      }, [pathname]);
    
      console.log({ status });
    
      return (
        <div className="App">
          <Navbar />
          <ToastContainer />
          <Routes>
            <Route index element={<Home />} />
            <Route path="/vehicle/:id" element={<EV />} />
            <Route element={<ProtectedRoute />}>
              <Route path="/add" element={<AddEV />} />
            </Route>
            <Route path="/filter" element={<Home />} />
            <Route path="/compare" element={<Home />} />
            <Route path="/login" element={<LoginRegister type="login" />} />
            <Route path="/register" element={<LoginRegister type="register" />} />
            <Route path="/logout" element={<Logout />} />
            <Route path="*" element={<p>Nothing here: 404!</p>} />
          </Routes>
        </div>
      );
    }