DEV Community

Harshal Pawshekar
Harshal Pawshekar

Posted on

Optimising Data loading in Remix

You are most likely aware of how to load data from API using a Loader function

const loader: LoaderFunction = async({request}) => {
  const response = await some_api(url);
  return json({data: response})
}
Enter fullscreen mode Exit fullscreen mode

in the component you can use useLoaderData hook to get the data.
Under the hood Remix will call the API first then render the component with the data then send it to the client (Server side data fetching)

If we were using a Vanilla React/Angular App then we would have Rendered the component first then started loading the data (Client side data fetching)

Problem with client side data fetching is that if you have nested component and each component shows a loading screen before rendering child components. Here user could have to wait for significant amount of time (more info here)

On top of that if you app has huge initial bundle size then this problem becomes even worse

So in our Remix app if we were to call all the API’s our component needs in loader function then is the problem solved ?

Well not exactly imagine a scenario where we need to call multiple API

const loader: LoaderFunction = async({request}) => {
  const res1 = await api1(url1);
  const res2 = await api2(url2);
  const res3 = await api3(url3);
  const res4 = await api4(url4);
  const res5 = await api5(url5);
  return json({res1, res2, res3, res4, res5})
}
Enter fullscreen mode Exit fullscreen mode

Here we need to call 5 API’s due to which when user visits the page he doesn't see anything because Remix is waiting for the loader function to return data
Here we can call the API in parallel instead of calling then in series we can use Promise.all but it accepts an array and returns an array if that is not convenient
then we can use promiseHash from remix-utils

const loader: LoaderFunction = async ({ request }) => {
  return json({
    data: await promiseHash({
      res1: api1(request),
      res2: api2(request),
      res3: api3(request),
      res4: api4(request),
      res5: api5(request),
    }),
  });
};
Enter fullscreen mode Exit fullscreen mode

promiseHash will resolve all promise in parallel
we can use .then() to transform the data if we want to

const loader: LoaderFunction = async ({ request }) => {
  return json({
    data: await promiseHash({
      res1: api1(request).then(foo => foo.bar).catch(e => console.error(e)),
      res2: api2(request).catch(e => console.error(e)),
      res3: api3(request).catch(e => console.error(e)),
      res4: api4(request).catch(e => console.error(e)),
      res5: api5(request).catch(e => console.error(e)),
    }),
  });
};
Enter fullscreen mode Exit fullscreen mode

Don't forget to add a catch block

But what if this is still not enough
Is there anything we can do to show some loading state while data is being fetched on the server ?

Well thanks to React 18’s Suspense we can stream components from the server while data is loading
to do this we can use defer instead of json from @remix-run/node

const loader: LoaderFunction = async ({ request }) => {
  return defer({
    data: promiseHash({
      res1: api1(request),
      res2: api2(request),
      res3: api3(request),
      res4: api4(request),
      res5: api5(request),
    }),
  });
};
Enter fullscreen mode Exit fullscreen mode

Here instead of resolving the promise on the server we resolve it on the client
so in the frontend we can use the data like this

const Component = () => {
  const response = useLoaderData();

  return <Suspense fallback={<div>Loading...</div>}>
    <Await 
    resolve={response.data} 
    errorElement={<div>Oppose something went wrong</div>}
    >
      {(data) => (
        <div>{data.res1}</div>
        <div>{data.res2}</div>
        <div>{data.res3}</div>
        <div>{data.res4}</div>
        <div>{data.res5}</div>
      )}
    </Await>
  </Suspense>
}
Enter fullscreen mode Exit fullscreen mode

Kindly read through the official guide here

Thats not it we can improve the user experience even more
the Link and NavLink accepts a prop prefetch by default it is set to none (read more here) but if we set it to intent Remix will load the data and component when the user hovers over the link
Browser will cache the JS files and and other assets but the API calls will not be cached by default this could lead to multiple API call to our backend

So we need to cache the API response
to do this we can use isPrefetch from remix-utils

const loader: LoaderFunction = async({request}) => {
  const response = await some_api(url);
  const headers = new Headers()
  if (isPrefetch(request)) {
    headers.set('Cache-Control', 'private, max-age=120); // expire in 2 minutes
  }
  return json({data: response}, {headers})
}
Enter fullscreen mode Exit fullscreen mode

we can use the headers with defer as well

In some case you can cache API response even if the request is not a prefetch

Top comments (0)