react-router
react-router copied to clipboard
[Feature]: Router Context
What is the new or updated feature that you are suggesting?
It would be useful to be able to set a context
value when initialising the router that can then be accessed in loaders
and actions
.
Example
// main.jsx
const queryClient = new QueryClient();
const router = createBrowserRouter(
[
{
path: '/',
element: <Root />,
loader: rootLoader
}
],
{
context: {
queryClient
}
}
);
// root.jsx
export async function loader({ params }, context) {
return await context.queryClient.fetchQuery(query);
}
Why should this feature be included?
As loaders
and actions
cannot use React hooks, there is currently no way to access contextual data within them. A workaround suggested in this article (https://tkdodo.eu/blog/react-query-meets-react-router) is to create an additional function wrapper for each loader
and action
. Providing a context
value directly when initialising the router would be a more elegant solution.
@jamesopstad Thanks for the request! This is definitely something on our radar and we've got a few APIs in mind that we're discussing internally. I'll update this issue as we have any more concrete information 👍
Hey @brophdawg11! I think this might be a related problem:
What are you supposed to do when you have a provider, say <AuthProvider>
that needs to live inside a router? It was simple with <BrowserRouter>
:
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
);
But not sure what the equivalent would be using v6.4 data APIs. This is not possible, since <AuthProvider>
needs to live inside of a router:
const router = createBrowserRouter(
createRoutesFromElements(<Route path="/" element={<Root />} />),
);
ReactDOM.createRoot(document.getElementById("root")).render(
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
This is not possible, since <AuthProvider>
is not a <Route>
component:
const router = createBrowserRouter(
createRoutesFromElements(
<AuthProvider>
<Route path="/" element={<Root />} />
</AuthProvider>,
),
);
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
This is possible:
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="/"
element={
<AuthProvider>
<Root />
</AuthProvider>
}
/>,
),
);
ReactDOM.createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
But that would require wrapping <AuthProvider>
on all top-level routes...
Any suggestions?
Just use a pathless Route
(with Outlet
inside of AuthProvider
):
const router = createBrowserRouter(
createRoutesFromElements(
<Route element={<AuthProvider />}>
<Route path="/" element={<Root />} />
{/* More Routes here... */}
</Route>
)
)
Just use a pathless
Route
(withOutlet
inside ofAuthProvider
):const router = createBrowserRouter( createRoutesFromElements( <Route element={<AuthProvider />}> <Route path="/" element={<Root />} /> {/* More Routes here... */} </Route> ) )
Easy as that! Thanks!
I've been test driving the new data loading apis, and I like the concept but in practice I'm finding it challenging to integrate with. It feels like a feature like this might help. The use case is simple. I've got an api that uses JWT auth. What's the best way to pass the current access token (and dealing with updating it etc) into a loader. The loaders are more or less pure functions it seems. The only example I see around auth, doesn't use the createBrowserRouter
api.
Maybe there's a way to do it, but wasn't clear through my investigation this morning. For a bit more context. I'm currently using the msal / msal-react
library which uses a context for providing auth type concerns.
My ideal scenario would be to be able to provide/update the access token that can be used by the data loaders for accessing protected data.
Interested to see / hear about what new api's might be in the works for the data loading side.
I wonder if this context would make sense sitting as a prop on the RouterProvider
which could re-execute loaders on change. The createBrowserRouter
function seems like it sits at a decidedly imperative place in the api, which makes sense since it needs to be able to know about all of the data loaders before the routes have actually been loaded.
@timdorr I tried your option, it only works fine, when you click around the site, but when I hit refresh, or enter direct page, the loader always crashes, since msalInstance is not initialised yet.
While this feature is being considered, I'd like to add that it'd be useful to provide a mechanism for passing values from hooks into loaders and actions.
As it stands, Auth0's React SDK library only allows token values to be accessed via the getAccessTokenSilently()
function, which is provided by the useAuth0()
hook. Auth0 currently doesn't provide a mechanism for getting a client instance, like with React Query's new QueryClient()
.
+1. I'd really like to migrate my app to Remix (or at least react-router 6.4), but I use custom Hooks to fetch all of my data. These Hooks use an apiRequest
function from a React context SessionProvider
- this is a wrapper around Axios that adds session-specific query params and headers to every request, and updates the session context afterwards. For example:
export const useGetUser = async (id: string) => {
const { apiRequest } = useSession();
return await apiRequest({
path: `identity/user/${id}`,
method: "GET",
})
};
I can't call these inside a loader. The best I can do is wrap them in Tanstack Query, and use the workaround mentioned by the OP here. Not ideal.
Because the session is stored inside of React context, the solution proposed in the OP isn't quite right for me either. In a perfect world, I could do something like this:
// App.tsx
const App = () => {
<SessionProvider>
{(sessionContext) => (
<RouterProvider context={{ sessionContext }}/>
)}
</SessionProvider>
}
and my loader could look like this:
// User.tsx
export const loader = async ({ params }, context) => {
const { apiRequest } = context.sessionContext
return apiRequest({
path: `identity/user/${id}`,
method: "GET",
})
}
Am I missing any workarounds? Would love some pointers if I'm going about this the wrong way.
Love the idea around loaders, but they are obviously not ready for anything more complex than todo app. There should be a way to pass additional data to the loaders somehow.
I would like to see some type of official context api added as well. Here is what I'm currently doing to ensure my loaders and actions have access to the firebase services. It would be great to remove the (args) => DashboardLoader({ ...args, context })
line and have context passed in.
import { LoaderFunctionArgs, ActionFunctionArgs } from "react-router-dom"
declare module "react-router-dom" {
interface LoaderFunctionArgs {
context: IFirebaseContext
}
interface ActionFunctionArgs {
context: IFirebaseContext
}
}
import AppShell from "@components/AppShell"
import Loader from "@components/Loader"
import { ROUTES } from "@constants"
import { useFirebase } from "@Firebase"
import DashboardRoute, { DashboardLoader } from "@routes/dashboard"
import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"
export const AppRouter = () => {
const { loading, ...context } = useFirebase()
// Wait for auth to finish loading
if (loading) {
return <Loader />
}
return (
<>
<RouterProvider
router={createBrowserRouter([
{
element: <AppShell />,
children: [
{
index: true,
path: ROUTES.Dashboard.slug,
element: <DashboardRoute />,
loader: (args) => DashboardLoader({ ...args, context }),
},
],
},
{
path: "*",
element: <Navigate to={ROUTES.Dashboard.path} />,
},
])}
/>
</>
)
}
export default AppRouter
There's a context
concept in https://github.com/remix-run/react-router/discussions/9564, but it won't currently suffice for passing data from a react context through. So I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!
Generally speaking though - we don't want React Context to be a dependency for data fetching - since the entire idea of the 6.4 data APIs is to decouple data fetching from rendering. In order to access client side data from a react context, it has to be handed off to a context provider somewhere higher up in the tree and by definition is then accessible from JS somehow. The solution is then to access that data from the source in JS, and not from context. But not all third party APIs currently give you easy access to some of this stuff in a non-context manner.