Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Making usePreloadedQuery throw a hard error when the environment changes will break React Router #4623

Open
steinybot opened this issue Feb 23, 2024 · 1 comment

Comments

@steinybot
Copy link

usePreloadedQuery will log a warning if it is passed a preloaded query that was created with a different environment than the one that is currently in context. It says that this will become a hard error in future. This seems reasonable however this is going to cause a pretty big problem when using relay with react-router.

Consider we have something like:

  <MutableRelayEnvironment>
     <MyRouter />
  </MutableRelayEnvironment>

Where MutableRelayEnvironment is something like:

export function MutableRelayEnvironment(props: Props) {
  const [environment, setEnvironment] = useState(createNewEnvironment);
  return <MutableRelayEnvironmentContext.Provider value={() => setEnvironment(createNewEnvironment())}>
    <RelayEnvironmentProvider environment={environment}>
      {props.children}
    </RelayEnvironmentProvider>
  </MutableRelayEnvironmentContext.Provider>;
}

That can be update elsewhere like:

export function CreateNewEnvironmentButton() {
  const createNewEnvironment = useContext(MutableRelayEnvironmentContext);
  return <button onClick={createNewEnvironment}>Create new environment</button>
}

And a naive router such as:

export function MyRouter() {
  const environment = useRelayEnvironment();
  const routes = preparePreloadableRoutes(MY_ROUTES, {
    getEnvironment() {
      return environment;
    },
  });
  const router = createBrowserRouter(routes);
  return <RouterProvider router={router} />;
}

createBrowserRouter will asynchronously run startNavigation that will load the data. It ends up yielding to the render of the RouterProvider which has a useLayoutEffect that will subscribe to the router to get the loaded data (which includes the preloaded queries). The first time around no data has been loaded and so the endpoint routes are not rendered. The layout effect is run and then it returns back to startNavigation which now provide the loaded data to each subscriber. The RouterProvider now rerenders with the new state and renders each route. Finally the routes call usePreloadedQuery and all is well.

The issue is that when the environment changes, MyRouter rerenders and calls createBrowserRouter again repeating the above process. However this time when startNavigation yields and RouterProvider renders it will have the data that was loaded last time which include the preloaded queries with the old environment. Loaded data exists and so the routes will be rendered this time. Now usePreloadedQuery will see that the environment from the context and the query do not match and we get the warning. RouterProvider will rerender like before and get a new set of preloaded queries with the updated environment and so is currently ok.

Once this becomes a hard error this is going to break pretty spectactularly and I can't think of a way for end users to avoid it except perhaps manually checking the environment and intentionally throwing and catching that in an error boundary at the root of the router and relying on it rerendering. That might work but the logs will still be full of errors since React does not provide a way to supress handled errors (facebook/react#15069).

I hope that it will be possible to work with react-router to make this work before making this a hard error in relay.

See here for the original issue that I reported with @loop-payments/react-router-relay: loop-payments/react-router-relay#14

Reproduction: https://github.com/steinybot/react-router-relay/blob/bug/stale-environment-2/examples/todo/src/MyRouter.tsx

@steinybot
Copy link
Author

I realised that the react router RouterProvider is behaving differently the second time around because the state has already been set. When the router is recreated it's state has initialized = false but the state of the RouterProvider has initialized = true. See remix-run/react-router#11300.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant