Stack Exchange
React Router v6, guarded routes using Context API
I have the following setup:
App.js
import AppContext from './context/AppContext.js';
import router from './routes/routes.js';

function App() {
    const [token, setToken] = useState("");
    
    function login(token) {
        setToken(token);
    }

    return (
        <React.StrictMode>
            <AppContext.Provider value={{ token, login }}>
                <RouterProvider router={router} />
            </AppContext.Provider>
        </React.StrictMode>
    );
}

export default App;

routes.js
export default createBrowserRouter([
    {
        path: "/",
        element: <Root />,
        children: [
            {
                path: "home",
                element: <Home />
            },
            {
                path: "admin",
                element: <ProtectedRoute element={Admin} />
            },
            {
                path: "login",
                element: <Login />
            }
        ]
    }
]);

ProtectedRoute.js
export default function ProtectedRoute({ element: Component, ...rest }) {
    const ctx = useContext(AppContext);

    return (
        ctx.token !== "" ? <Component /> : <Navigate to="/login" />
    );
}

I want to be able check if the user is logged in by accessing information from a context, however the returned object always contains the default values. Is there a way to access context information with createBrowserRouter in this scenario?

The protected route component should be refactored to be rendered as a layout route component by rendering an Outlet instead of some passed prop.
import { Navigate, Outlet } from 'react-router-dom';

export default function ProtectedRoute() {
  const { token } = useContext(AppContext);

  return token
    ? <Outlet />
    : <Navigate to="/login" replace />;
}

export default createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      { path: "home", element: <Home /> },
      {
        element: <ProtectedRoute />,
        children: [
          {
            path: "admin",
            element: <Admin />
          },
          // ... other protected routes
        ],
      },
      { path: "login", element: <Login /> }
      // ... other unprotected routes
    ]
  }
]);


I see. So every protected route should be placed inside of the children array of the <ProtectedRoute> element. That worked, thank you very much! :)

@navix1337 Affirmative. I've added some code comments to make that more clear.

Hmm, I just tried this in my production code. The Login component can access the AppContext and extracts the token correctly, the ProtectedRoute component still gets an empty string as the token.

@navix1337 It should work the same in both locations. Think you could create a running codesandbox demo that reproduces that issue we could inspect live?

Sure: https://codesandbox.io/s/agitated-driscoll-2cnwxl.
I think the asynchronous request is causing the problem. If I mock the request synchronously inside the handleLogin function (i.e. remove the artificial promise) it redirects to the original page just fine.

@navix1337 Yeah, I think this might be related to the way React handles state updates in asynchronous callbacks, but from what I understand of React 18 they should still be batched. I typically use Redux and Thunks for asynchronous actions, but logically it should be about the same, just on a much simpler scale with a manual fetch in the login handler. The setTimeout(cb, 0) trick is usally enough to kick the navigate call to the end of the event queue though.
