Search code examples
reactjsuser-interfacereact-router-dom

How do I protect a nested route, that is nested in a protected route, with react-router-dom?


I protect /dashboard/* routes with a RequireAuth outlet component. RequireAuth verifies a jwt token, and this works as expected. Navigating either with the url bar, or a react-router-dom component, from an unprotected route to a protected route verifies the token before granting access.

However, once I have landed on a 'dashboard/*' component I am able to navigate within those nested components using a without re-verifying the token (if the url is typed directly in then the token is verified.

How can I alter the behavior so that:

1.) user navigates from /login to /dashboard

2.) token is verified

3.) user navigates from /dashboard to /dashboard/new-path

4.) token is verified

App.jsx routes

      <Routes>
        {/* protected routes */}
        <Route element={<RequireAuth />}>
          <Route path="dashboard/*" element={<Dashboard />} />
        </Route>

        {/* public routes */}
        <Route index element={<Public />} />
        <Route path="login" element={<Login />} />
        <Route path="register" element={<Register />} />
      </Routes>

Dashboard.jsx routes

    <div>
      <Navbar />
      <Routes>
        <Route path="/" element={<DashboardTimeline />} />
        <Route path="/my-paths" element={<MyPaths />} />
        <Route path="/new-path" element={<NewPathWizard />} />
      </Routes>
    </div>

RequireAuth.jsx

import { useState, useEffect } from 'react';
import { useLocation, Navigate, Outlet } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { selectCurrentToken, logOut } from './authSlice';
import { useVerifyTokenMutation } from './authApiSlice';
import FullScreenLoading from '../../components/general/FullScreenLoading';

function RequireAuth() {
  const [authenticated, setAuthenticated] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const location = useLocation();
  const [verifyToken] = useVerifyTokenMutation();
  const token = useSelector(selectCurrentToken);
  const dispatch = useDispatch();

  useEffect(() => {
    const authenticateToken = async () => {
      const response = await verifyToken({ token }).unwrap();
      const { verified } = response.data;
      setIsLoading(false);
      setAuthenticated(verified);
    };

    if (token) {
      authenticateToken()
        .catch((error) => {
          const { verified } = error.data;
          dispatch(logOut());
          setIsLoading(false);
          setAuthenticated(verified);
        });
    } else {
      setIsLoading(false);
      setAuthenticated(false);
    }
  }, []);

  if (isLoading === true) {
    return <FullScreenLoading />;
  }

  return (
    authenticated && !isLoading
      ? <Outlet />
      : <Navigate to="/" state={{ from: location }} replace />
  );
}
export default RequireAuth;

I have tried wrapping the nested routes within Dashboard.jsx in the RequireAuth component as well, but it made no difference.


Solution

  • If you want the user's authentication/token status checked for each navigation then you can add the current location's pathname as a dependency for the useEffect hook. When the location.pathname changes, the effect will be run.

    Example;

    function RequireAuth() {
      const [authenticated, setAuthenticated] = useState(null);
      const [isLoading, setIsLoading] = useState(true);
    
      const location = useLocation();
      const { pathname } = location; // <-- access current pathname
    
      const [verifyToken] = useVerifyTokenMutation();
      const token = useSelector(selectCurrentToken);
      const dispatch = useDispatch();
    
      useEffect(() => {
        const authenticateToken = async () => {
          setIsLoading(false); // <-- start/restart loading condition
          try {
            const response = await verifyToken({ token }).unwrap();
            const { verified } = response.data;
            setAuthenticated(verified);
          } catch(error) {
            const { verified } = error.data;
            dispatch(logOut());
            setAuthenticated(verified);
          } finally {
            setIsLoading(false); // <-- clear loading when finished
          }
        };
    
        if (token) {
          authenticateToken();
        } else {
          setIsLoading(false);
          setAuthenticated(false);
        }
      }, [pathname]); // <-- add as dependency
    
      if (isLoading) {
        return <FullScreenLoading />;
      }
    
      return (
        authenticated && !isLoading
          ? <Outlet />
          : <Navigate to="/" state={{ from: location }} replace />
      );
    }
    
    export default RequireAuth;