Search code examples
reactjstypescriptreact-routerreact-router-dom

React router context not reaching protected route


This is my index.tsx

import ReactDOM from 'react-dom/client';
import App from './App';
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
import { msalConfig } from './azureAuthConfig';

const msalInstance = new PublicClientApplication(msalConfig);

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  <MsalProvider instance={msalInstance}>
    <App />
  </MsalProvider>
);

This is my app.tsx

import { BrowserRouter, Routes, Route } from "react-router-dom"
import './App.css';
import Layout from './components/Layout';
import Home from './components/Home';
import AuthRequired from './components/AuthRequired';
import Usage from "./components/Usage";
import Login from "./components/Login";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route element={<AuthRequired />} >
            <Route path="usage" element={<Usage />} />
          </Route>
          <Route path="/login" element={<Login />} />
        </Route>
      </Routes>
    </BrowserRouter >
  );
}

And this is my Layout component where I have defined some outlet context.

import React from 'react';
import Header from "./Header"
import Footer from "./Footer"
import { Outlet } from 'react-router-dom';
import { ReportingPeriod, SelectedProjects } from '../types';
import { DefaultReportingPeriod } from '../utils';

export default function Layout() {
  // Set Context for the Pollination key
  const [pollinationKey, setPollinationKey] = React.useState<string>("")

  // Set reporting period to be used on the rest of the app
  const [reportingPeriod, setReportingPeriod] = React.useState<ReportingPeriod>({
    start: DefaultReportingPeriod().start,
    end: DefaultReportingPeriod().end
  });

  // Set Project names to be used on the rest of the app
  const [selectedProjects, setSelectedProjects] = React.useState<SelectedProjects>({});

  return (
    <>
      <Header />
      <Outlet
        context={{
          pollinationKey, setPollinationKey,
          reportingPeriod, setReportingPeriod,
          selectedProjects, setSelectedProjects
        }}
      />
      <Footer />
    </>
  )
}

I see that I can access this context successfully on Login route but not on the Usage route which is a protected route. Why would that be?

Here's the error I get when I try to access one of the context values

Uncaught TypeError: Cannot destructure property 'reportingPeriod' of '(0 , react_router_dom__WEBPACK_IMPORTED_MODULE_5__.useOutletContext)(...)' as it is undefined.

I tried the following:

  1. As suggested by documentation, I did create a hook on the Layout component to consume in the Usage component. Didn't work. Same result.

  2. I tried taking the Usage route out of the protected route just to test. It worked. I was able to access the value.

  3. If I use React's Context API and wrap the BrowserRouter with a context provider like shown below. It works.

    export default function App() {
      return (
        <AppProvider>
          <BrowserRouter>
            <Routes>
              <Route path="/" element={<Layout />}>
                <Route index element={<Home />} />
                <Route element={<AuthRequired />} >
                  <Route path="usage" element={<Usage />} />
                </Route>
                <Route path="/login" element={<Login />} />
              </Route>
            </Routes>
          </BrowserRouter >
        </AppProvider>
      );
    }
    

As mentioned above, I would really like to achieve this functionality using the React Router V6's outlet context.


Solution

  • Both Layout and AuthRequired should be rendering Outlets components if implemented normally. The issue would appear to be that you are destructuring reportingPeriod via useOutletContext directly in the leaf route component, e.g. Usage. The useOutletContext hook accesses the context value of the closest ancestor Outlet component, e.g. the one provided by AuthRequired.

    You can update AuthRequired to access any ancestor Outlet context and forward on down the ReactTree.

    Example:

    export default function Layout() {
      ...
    
      return (
        <>
          <Header />
          <Outlet
            context={{
              pollinationKey,
              setPollinationKey,
              reportingPeriod,
              setReportingPeriod,
              selectedProjects,
              setSelectedProjects
            }}
          />
          <Footer />
        </>
      )
    }
    
    const AuthRequired = () => {
      const context = useOutletContext(); // <-- any ancestor context value
    
      ... AuthRequired business logic ...
    
      return <someCondition>
        ? (
          <Outlet
            context={{
              ...context, // <-- shallow copy ancestor context value
              // add any new context value
            }}
          />
        ) : <Navigate to="/login" replace />;
    };
    
    export default function App() {
      return (
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<Layout />}>
              <Route index element={<Home />} />
              <Route
                element={<AuthRequired />} // <-- consumes Layout outlet context 
              >
                <Route
                  path="usage"
                  element={<Usage />} // <-- consumes AuthRequired outlet context 
                />
              </Route>
              <Route path="/login" element={<Login />} />
            </Route>
          </Routes>
        </BrowserRouter >
      );
    }
    
    const Usage = () => {
      const { reportingPeriod } = useOutletContext();
    
      ...
    };