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

Fetchers trigger unnecessary useLocation/LocationContext updates #3672

Closed
dmarkow opened this issue Jul 7, 2022 · 4 comments
Closed

Fetchers trigger unnecessary useLocation/LocationContext updates #3672

dmarkow opened this issue Jul 7, 2022 · 4 comments

Comments

@dmarkow
Copy link
Contributor

dmarkow commented Jul 7, 2022

What version of Remix are you using?

1.6.3

Steps to Reproduce

Sample component below. One child component calls a fetcher, the other child component calls useLocation. The location component renders 3 times total, even when using React.memo. However, the actual value of useLocation stays the same.

import { useFetcher } from "@remix-run/react";
import React, { useContext, useEffect } from "react";
import { UNSAFE_LocationContext, useLocation } from "react-router-dom";

export default function TestRoute() {
  return (
    <div>
      <DataComponent />
      <LocationComponent />
    </div>
  );
}

const LocationComponent = React.memo(() => {
  const locationContext = useContext(UNSAFE_LocationContext);
  const location = useLocation();

  // This prints 3 times: once on the initial page load, once when the fetcher
  // starts loading, and once when the fetcher is done loading.
  useEffect(() => {
    console.log("LocationContext changed");
  }, [locationContext]);

  // useLocation's value doesn't change, this only prints once during the
  // initial render.
  useEffect(() => {
    console.log("Location changed");
  }, [location]);

  // This also prints 3 times with the exact same output, even if we remove the
  // useContext call above.
  //
  // Even though useLocation doesn't change, react devtools say this component
  // is rendering due to "context changes". Removing the useLocation() call 
  // makes this render just once.
  console.log("Location: ", JSON.stringify(location));
  return <div />;
});

const DataComponent = () => {
  const fetcher = useFetcher();
  useEffect(() => {
    fetcher.load("/resources/users");
  }, []);

  return <div>{JSON.stringify(fetcher.data)}</div>;
};

Expected Behavior

The useFetcher docs specifically say "Because useFetcher doesn't cause a navigation...". I would assume not causing navigation means that useLocation() shouldn't change.

Actual Behavior

useLocation() causes unnecessary re-renders when using fetchers, which can be costly on large/complex pages.

Digging in, it seems that https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx#L235 may be the culprit? Even though location and navigationType aren't actually changing, it's creating a new object with value={{ location, navigationType }} any time the Router component re-renders.

I tested patching this to wrap the context value with useMemo and the re-renders stopped:

let locationValue = useMemo(() => ({ location, navigationType }), [location, navigationType]);
return (
  <NavigationContext.Provider value={navigationContext}>
    <LocationContext.Provider
      children={children}
      value={locationValue}
    />
  </NavigationContext.Provider>
);

I was unsure if this issue is Remix-specific with the fetchers causing unnecessary updates of LocationContext, of if this bug needs to be addressed in react-router instead?

@wanbinkimoon
Copy link

wanbinkimoon commented Jul 13, 2022

I reproduced the error by adding the route /resources/user which is called in the useEffect

here a test repo https://github.com/wanbinkimoon/remix-use-fetcher-rerender-testing

Screenshot 2022-07-13 at 17 27 50

@JClackett
Copy link

JClackett commented Dec 8, 2022

This is something i'm experiencing and its causing really slow client navigation. Here's my situation:

I have a table of items that can be infinitely paginated.

On this same page I have a few links to "modal routes".

In each item component I have a few "useFetcher" hooks that are used to submit things to certain actions (duplicate, delete, archive etc).

When opening any of the modal routes, the url updates, and I can see that the useFetcher hook is causing each item to re-render. This is causing the modal routes to render really slowly, >2seconds as remix/react is re-rendering every single one of the items.

I don't get why the useFetcher hooks, which aren't dependent on the URL/location, need to cause these updates.

    "@remix-run/express": "1.8.2",
    "@remix-run/node": "1.8.2",
    "@remix-run/react": "1.8.2",

p.s because I use "useFetcher" all over the place, it causes so many things to re-render when they really dont need to so, I've had to put React.memo everywhere too to limit the damage and stop all children components re-rendering. But there's nothing I can really do about the component that uses the hook directly.

Thoughts?

@JClackett
Copy link

Seems like this line is still causing the issue:

https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/components.tsx#L355

do the objects need to be memoized?

@dmarkow
Copy link
Contributor Author

dmarkow commented Mar 9, 2023

remix-run/react-router#9983 was merged which should fix this when that release is published.

@dmarkow dmarkow closed this as completed Mar 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants