Sharing state across React router v6 routes using Context API
Asked Answered
I

2

5

Looking through old tutorials it seems like React Router v5 had support for sharing state across different routes using the context API but I can't find anything on a similar usage for Router v6.

React Router v5 Implementation of what I am trying to do:

const [user, setUser] = useState(null);
 return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
          </ul>
        </nav>
        <UserContext.Provider value={user,setUser}>
          <Route path="/" exact component={Index} />
          <Route path="/about/" component={About} />
        </UserContext.Provider>
      </div>
    </Router>
  );

and then you can access the state using the useContext hook

const {value, setValue} = useContext(UserContext);

v6 Implementation

When attempting to use this implementation with v6 (exchanging the degraded v5 components for the new v6 ones) you will run into errors because you can only have <Route> components as children in a router.

Is it possible to share state with a global context store across React Router V6 routes?

Below is my attempt at V6 implementation:

index.js

import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);

App.js

const [value, setValue] = useState("initial state");

  return (
    <>
      <Header props={(key, setKey)} />
      <DataContext.Provider value={(value, setValue)}>
        <Route path="/" element={<Dashboard />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/configuration" element={<Configuration />} />
      </DataContext.Provider>
    </>
  );

App.js different Approach

  const [value, setValue] = useState("initial state");

  return (
    <DataContext.Provider value={(value, setValue)}>
      <Header props={(key, setKey)} />
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/configuration" element={<Configuration />} />
      </Routes>
    </DataContext.Provider>
  );
}

The issue with this solution is the state is still not updated globally when changed in one of the routes. For example in /dashboard if I change value using setValue then it will reflect the changes in /dashboard but if I navigate to /configuration or refresh value will have reverted to original value, "initial state" in this case. (This is also the effect if I make a function in App.js which will use setValue and I pass the function to Provider instead)

I am sure that I could use React Redux to solve this problem but its really just one or two pieces of data that I need shared between routes - seems like overkill to implement all of the required redux boilerplate etc. and seems like a common use case that context should support.

Improvise answered 29/1, 2022 at 16:44 Comment(3)
I don't really understand what the issue is. Any React context has nothing to do with routing/navigation. Your second App.js approach appears correct, i.e. you've wrapped the components that consume the context value and you've correctly wrapped the Route components in the Routes component. What isn't working as expected?Mairamaire
Apologies for the lack of clarity, I have added a short example and better description to the bottom of the question.Improvise
Your "App.js different Approach" should still work. Have you included all relevant code in your question?Mairamaire
S
6

Its simple you need to use Router inside your DataProvider.

index.js

// import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
  <App />,
  document.getElementById("root")
);

app.js

import { BrowserRouter as Router } from "react-router-dom";
// .....
// .....
// .....
 const [value, setValue] = useState("initial state");

  return (
    <DataContext.Provider value={(value, setValue)}>
      <Header props={(key, setKey)} />
      <Router>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/configuration" element={<Configuration />} />
      </Routes>
      </Router>
    </DataContext.Provider>
  );
}

Also make sure you are using the Link of router to navigate to different pages. Using any other thing 'a' tag, etc. that refreshes the page will reset the context.

Schumann answered 29/1, 2022 at 17:19 Comment(7)
With the solution given the state is still not updated globally when changed in one of the routes. For example in /dashboard if I change value using setValue then it will reflect the changes in /dashboard but if I navigate to /configuration or refresh value will have reverted to original value. (This is also the effect if I make a function in App.js which will use setValue and I pass the function to Provider instead)Improvise
How are you navigating to different route? if you are using a tag or anything that requires reloading like window.location = '/home' then the problem lies there.Schumann
This was the issue ^ I was not using the router Link component I was using a bootstrap Nav.Link component. Thank you for the help - A full example of the React Router code that helped me re-factor can be found here: stackblitz.com/github/remix-run/react-router/tree/main/examples/…Improvise
Update this to the answer.Schumann
This is fine if you want to wrap ALL of your routes in the DataContext, but what if you only want to wrap the Context around some nested routes?Addend
grayson, I searched and found out this comment on Reddit that helps to clarify how to wrap a context provider around a few routes: reddit.com/r/reactjs/comments/uxvfvj/comment/ia0g5mx/… In short, you should put your context provider inside a component that uses either Outlet or useOutlet from React Router. Then, use the component as the element of a Route, which is the parent Route of the few and only routes you want the context to be available.Legislation
This might help->#72764523Snider
S
4

All the answers above are accurate but you need to make a tiny little change if you are using RouterProvider in react-router-dom-v6 and have defined your routes inside a RouterProvider like <RouterProvider router={router} />, wrap your RouterProvider inside your ContextProvider to make the context accessible.

I hope the code below clarifies what I am suggesting.

App.jsx

import { 
Route, 
Link, 
Routes, 
createBrowserRouter, 
createRoutesFromElements, 
RouterProvider 
} from 'react-router-dom';
import { AuthProvider } from './Components/context/AuthContext';
....
import PrivateRoute from './Components/utils/PrivateRoute';

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path='/' element={<Layout />}>
      .... other Routes
     )
)

function App() {
  


  return (
    <AuthProvider>
      <RouterProvider router={router} />
      </AuthProvider>
  )
}

export default App

I made a separate module for Context just to keep the App.jsx clean.

AuthContext.jsx

import { createContext, useState, useEffect } from "react";
import { axiosInstance } from "../Hooks/AxiosInst";


const AuthContext = createContext()


export default AuthContext;


export const AuthProvider = ({children}) => {

const [authToken, setToken] = useState(null)
const [user, setUser] = useState(null)

const authLogin = async () //....
  

let contextData = {
    user: user,
    login:authLogin
}

return (
    <AuthContext.Provider value={contextData}>
        {children}
    </AuthContext.Provider>
)
}
Staats answered 29/6, 2023 at 10:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.