react-router icon indicating copy to clipboard operation
react-router copied to clipboard

[Feature]: Router Context

Open jamesopstad opened this issue 2 years ago • 10 comments

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 avatar Sep 22 '22 12:09 jamesopstad

@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 👍

brophdawg11 avatar Sep 22 '22 14:09 brophdawg11

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?

filiptammergard avatar Oct 05 '22 11:10 filiptammergard

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

timdorr avatar Oct 05 '22 14:10 timdorr

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>

  )

)

Easy as that! Thanks!

filiptammergard avatar Oct 05 '22 16:10 filiptammergard

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.

cjam avatar Oct 18 '22 14:10 cjam

@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.

slowWriting avatar Oct 28 '22 12:10 slowWriting

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().

jordanthornquest avatar Nov 02 '22 23:11 jordanthornquest

+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.

lukebelliveau avatar Nov 03 '22 21:11 lukebelliveau

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.

Yankovsky avatar Nov 08 '22 20:11 Yankovsky

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

j-bullard avatar Nov 18 '22 21:11 j-bullard

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.

brophdawg11 avatar Jan 09 '23 22:01 brophdawg11