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.

yaboi-hugh
  • 163
  • 3
  • 10
  • 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? – Drew Reese Jan 30 '22 at 00:24
  • Apologies for the lack of clarity, I have added a short example and better description to the bottom of the question. – yaboi-hugh Jan 30 '22 at 12:33
  • Your "App.js different Approach" should still work. Have you included all relevant code in your question? – Drew Reese Jan 30 '22 at 23:58

2 Answers2

5

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.

Shubham Kumar
  • 538
  • 5
  • 12
  • 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) – yaboi-hugh Jan 30 '22 at 12:26
  • 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. – Shubham Kumar Jan 30 '22 at 15:44
  • 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: https://stackblitz.com/github/remix-run/react-router/tree/main/examples/basic?file=src/App.tsx – yaboi-hugh Jan 30 '22 at 20:18
  • Update this to the answer. – Shubham Kumar Jan 31 '22 at 09:23
  • 2
    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? – grayson Apr 30 '22 at 02:31
  • grayson, I searched and found out this comment on Reddit that helps to clarify how to wrap a context provider around a few routes: https://www.reddit.com/r/reactjs/comments/uxvfvj/comment/ia0g5mx/?utm_source=share&utm_medium=web2x&context=3 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. – marcel099 Nov 23 '22 at 14:34
  • This might help->https://stackoverflow.com/questions/72764023/how-do-i-wrap-2-routes-with-a-context-in-react-router-v6 – chamara Feb 12 '23 at 07:52
2

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>
)
}