reactjstypescriptreact-routerreact-router-dom

While using defer, useLoaderData() returns the previous fetched results instead of a promise


I am experiencing a problem with useLoaderData() hook, While using react-router Form component with GET method. I have programmed the loader to return deferred data, and I am using Suspense in my component to render a Loading icon until the promise is resolved. This works as intended on the initial render, i.e:

However, when i submit the form again, the following happens:

App component(Router):

function App() {
  const browserRouter = createBrowserRouter(
    createRoutesFromElements(
      <Route
        path="/"
        element={<HomePage />}
        loader={HomePageLoader}
      />
    )
  );

  return (
    <>
      <Navbar />
      <RouterProvider router={browserRouter} />
    </>
  );
}

Function interacting with API()

export async function fetchDataFromColorAPI(targetUrl: string) {
  const response = await fetch(targetUrl);
  if (response.status !== 200) {
    throw new Error("Error occurred with API");
  }
  const data = await response.json();
  console.log("API data:  ",data.colors);
  return getColorsInfo(data.colors);
}

Loader Function:

export function loader({ request }: LoaderFunctionArgs) {
  console.log("in loader");
  const params = new URL(request.url).searchParams;
  const { color, mode, count } = getSearchParams(params); //utility function to get search parameters
  const baseUrl = "https://www.thecolorapi.com/scheme";
  const searchQuery = `?hex=${color}&mode=${mode}&count=${count}&format=json`;

  return defer({ colorsInfo: fetchDataFromColorAPI(baseUrl + searchQuery) });
}

Component calling useLoaderData():

export default function HomePage() {
  console.log("In component");

  const data = useLoaderData() as LoaderData;
  function renderPallet(colorsInfo: Color[]) {
    console.log("promise resolved, Values recieved: ", colorsInfo);
    return <Pallet colorsInfo={colorsInfo} />;
  }

  return (
    <main>
      <InputForm />
      <Suspense fallback={<h1>Loading....</h1>}>
        <Await resolve={data.colorsInfo}>{renderPallet}</Await>
      </Suspense>
    </main>
  );
}

Form:

<Form  replace className="form">
  //Input fields
</Form>

I tried going through the docs, but couldn't find the reason for this. My guess is that this may be happening because of some optimization being done by React or React-Router. Can someone explain this behaviour specifically how loader and useLoaderData() behave when combined with React-Router Form "GET" method. How can I force the useLoaderData() hook to return a promise instead of the previous fetched result.

Github Respository: https://github.com/Azfar731/ColorPicker.git


Solution

  • Once a route is loaded, it's not going to re-load unless you navigate away first. In other words, once a route has loaded the first time, any additional actions can only re-render the already mounted route component and re-run any route loaders instead of doing a full loading/mounting cycle.

    After route actions are called, the data will be revalidated automatically and return the latest result from your loader.

    Note that useLoaderData does not initiate a fetch. It simply reads the result of a fetch React Router manages internally, so you don't need to worry about it refetching when it re-renders for reasons outside of routing.

    This also means data returned is stable between renders, so you can safely pass it to dependency arrays in React hooks like useEffect. It only changes when the loader is called again after actions or certain navigations. In these cases the identity will change (even if the values don't).

    This matches what you are observing where the previously fetched result is still provided to the component until it's replaced by the next fetch.

    I think you can use the useNavigation hook to handle when the route's loader is fetching in response to a route or form action being triggered. See specifically navigation.state for complete details.

    Example:

    import { ...., useNavigation } from 'react-router-dom';
    
    export default function HomePage() {
      const data = useLoaderData() as LoaderData;
      const navigation = useNavigation();
    
      // Are we reloading after an action?
      const isReloading =
        navigation.state === "loading" &&
        navigation.formData != null &&
        navigation.formAction === navigation.location.pathname;
    
      // Are we redirecting after an action?
      const isRedirecting =
        navigation.state === "loading" &&
        navigation.formData != null &&
        navigation.formAction !== navigation.location.pathname;
    
      function renderPallet(colorsInfo: Color[]) {
        console.log("promise resolved, Values received: ", colorsInfo);
        return <Pallet colorsInfo={colorsInfo} />;
      }
    
      return (
        <main>
          {(isReloading || isRedirecting) && <h1>Loading....</h1>}
          <InputForm />
          <Suspense fallback={<h1>Loading....</h1>}>
            <Await resolve={data.colorsInfo}>{renderPallet}</Await>
          </Suspense>
        </main>
      );
    }