Resilient and Performant Data Fetching in Next.js
Tags: webdev, nextjs, javascript, react
Published: 2022-11-29 16:35:30

_A tutorial on how to use the best of React Query and Next.js to optimize API calls and render data to a view_

APIs are usually our primary data source for driving our website’s views. They are a core part of how our application presents information, such as the weather, user data, lists of transactions, you name it! Given the importance of fetching data and rendering it, we must ensure we do this in a performant and resilient manner. Server errors, network issues, request timeouts, and other factors could prevent your data from populating your views. Luckily, there are a few tools we can use to optimise our initial requests and continuously update data when it becomes stale.

## Next.js Server-Side Fetching
What’s better than showing a well-designed loading spinner while you fetch data? Not needing one at all!

[Next.js](https://nextjs.org/) offers a page-level function called [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props), which allows us to perform a NodeJS fetch request and pass it as props to the page. This gives us the benefit of having data ready on our client side.

Fetching and rendering a list of companies from the [Faker API](https://fakerapi.it/en) may look like this:

```jsx
const fetchCompanies = async () => {
  const res = await fetch("https://fakerapi.it/api/v1/companies");

  if (!res.ok) {
    throw new Error("Something went wrong");
  }

  const { data = [] } = await res.json();

  return data;
};

export default function Home({ data }) {
  return (
    <div>
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};

export const getServerSideProps = async () => {
  const data = await fetchCompanies()
  return {
    props: {
      data
    },
  };
};
```

## Page View

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5c36179b26p0vq4hmovz.jpg)

Looks great, right? The list renders almost instantly, and we don’t even need to show a loading state (for now).

If we lived in a perfect world, this might be enough to ship to production. However, in a realistic scenario, we have to account for initial requests failing, needing to retry, caching for performance, and avoiding hitting the API limit.

## React Query to the Rescue
[React Query](https://tanstack.com/query/v4/) is an excellent data fetch library that allows us to use initial server-side fetches, retries, caching, request timeouts, and more!

The first improvement we could make to the above code is introducing a query hydration and retry mechanism. Hydration is the process of using client-side JavaScript to add application state and interactivity to server-rendered HTML.

To get started, we need to wrap our pages in React Query’s providers inside `_app.js`.

```jsx
import { QueryClient, QueryClientProvider, Hydrate } from "@tanstack/react-query";
// Create a client
const queryClient = new QueryClient();
export default function MyApp({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  );
}
```

Two things are happening here. First, we’re wrapping our app with a `QueryClientProvider`, which gives React Query’s hooks access to an instance of `QueryClient` via context. Secondly, we are passing a dehydrated state to `Hydrate`. The dehydrated state, which will come from our server-side fetch, is a frozen representation of a cache that can later be hydrated on the client side.

## Revising our Initial Approach

```jsx
import { QueryClient, dehydrate } from "@tanstack/react-query";

const fetchCompanies = async () => {
  const res = await fetch("https://fakerapi.it/api/v1/companies");

  if (!res.ok) {
    throw new Error("Something went wrong");
  }

  const { data = [] } = await res.json();

  return data;
};

export default function Home() {
  const { data, error, isLoading } = useQuery({ queryKey: 'companies', queryFn: fetchCompanies, staleTime: 60_000 }); // stale after 1 min
  if (isLoading){
   return <h1>Loading...</h1>
  }
  if (error){
   return <h1>{error.message}</h1>
  }
  return (
    <div>
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};

export const getServerSideProps = async () => {
  const queryClient = new QueryClient()
  await queryClient.prefetchQuery(["companies"], fetchCompanies);

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
};
```

## What has changed?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1fx134enw0ku1lr2irvf.jpg)

If we look at our preliminary page load, we’ve still got our rendered list of companies and no sign of any client-side fetch requests. This means we’re still fetching server-side initially.

Our benefit is that our data will refresh client-side when it goes stale. Data could go state due to our cache expiring, us indicating a timeout for fresh data, or performing another request (e.g., creating a new data entry with a `POST` request), which triggers an invalidation of the current data.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1b7xf1d07aq6ux191di.jpg)

Rechecking the network tab, we can see that a client-side fetch is triggered as soon as the data is considered stale. As a result, React Query will attempt to recover data multiple times if the initial request fails.

## Conclusion (almost)
That’s it! We’ve made our application performant and reactive towards request failures. Additionally, React Query can be configured to further optimize application state and requests, depending on the type of application you’re developing.

Having said that, there is one more thing we can do…

## Refactoring
Let’s clean up this code and turn it into something much nicer.

We can start by defining an enum for our keys and a function mapping for our fetchers.

```jsx
const QUERY_KEYS = {
  COMPANIES: "companies",
}
const queryFunctions = {
  [QUERY_KEYS.COMPANIES]: fetchCompanies,
};
```

After that, we can take our `useQuery` hook and create a HOC ([higher-order component](https://reactjs.org/docs/higher-order-components.html)) with it.

```jsx
export const withQuery = (Component, key) => {
  return (props) => {
    const queryResponse = useQuery({ queryKey: [key], queryFn: queryFunctions[key], staleTime: 50_000 });
    return <Component {...{...props, ...queryResponse}} />;
  };
};
```

This HOC will wrap our page component and spread the query response props (along with page props) for us, similar to how we used the data we received directly from `getServerSideProps` in our first iteration. This gives us the benefit of having a simple interface and abstracting the implementation details.

Our final page component will look like this:

```jsx
const Home = ({ data, error, isLoading }) => {

  if (isLoading) {
    return <h1>Loading...</h1>;
  }

  if (error) {
    return <h1>{error.message}</h1>;
  }

  return (
    <div>
      <ReactQueryDevtools initialIsOpen={false} />
      <h1>Companies</h1>
      <ol>
        {data && data.map(({ id, name, email, vat, phone, website }) => (
          <li key={id}>
            <h2>{name}</h2>
            <p>{email}</p>
            <p>{vat}</p>
            <p>{phone}</p>
            <p>{website}</p>
          </li>
        ))}
      </ol>
    </div>
  );
};


export default withQuery(Home, QUERY_KEYS.COMPANIES);

```

We could also, of course, extend this to use multiple queries, pass additional configurations to `useQuery`, etc., but for now, this will suffice.

I hope this gives you some insight into how to increase the resilience and performance of your API requests inside Next.js.

Happy fetching! 🔥