Data fetching patterns - balancing client and server with SWR and Next.js
Tags: webdev, typescript, nextjs
Published: 2023-01-13 05:33:54


With the advent of modern component JS-based full-stack frameworks, we now have the flexibility to get remote data into our apps either via `client` or `server`.

**Why does this matter?**

Based on how we get data into our system we make different trade-offs with core web vitals.

**For example -**

If we get all the data from the server we would have to wait for all the data to be available. So the user would have to wait for more time to see something on the screen ([TTFB](https://web.dev/ttfb/)).

**So should we shift all the network calls on to the client then 🤔 ?**

Lots of loading spinners! 🌀

**Well turns out the answer is nuanced - not surprising at all is it 😃.**

A good rule of thumb would be if the information is above the fold or critical we shouldn’t make the user see spinners as much as possible. And if something is quite hidden from the initial UI can be potentially loaded “lazily”.

But that means some calls we need to make some calls on the server and some on the client. That requires granular control over which calls need to be executed on the client and which on the server. And hopefully with little or no extra code.

**Also what about data changing over time?**

Perhaps something like product availability in an e-commerce application that we can load initially at the time from the server but can potentially change while the user is browsing? We would need some ability to re-validate 🤔.

To solve these problems we have some awesome libraries namely:

- [SWR](https://swr.vercel.app/docs/data-fetching)
- [TanStack Query](https://tanstack.com/query/v4/)

I have chosen `Next.JS` as the framework and `SWR` as the library to showcase different ways to leverage the different options at our disposal.

I have created a demo `weather` app to showcase different variations of data fetching.

Looks something like this:


![Sample App Image 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/06z7a71qb08jss1k8bpe.png)


Lets dig in!

## Pure Client Side

This is the default approach with `swr` all you need to do is use the hook and provide it with a unique `key` to identify the remote call.

In this case we will use the `location` as the `key` so the code will look something like this:

```tsx
function Weather() {
  // first param is the key and second argument is custom fetcher.
  // the key is passed to custom fetcher.
  const { data, error } = useSWR(location, getWeatherInfo);
  if (error) {
    return <p>Error!</p>;
  }
  if (!data) {
    return <p>Loading..</p>;
  }
  return (
    <>
      <p>{data.temperature}</p>
      <p>{data.humidity}</p>
    </>
  );
}
```

I have used the same custom fetch client that I blogged about last time:

[**Making impossible states impossible ft. Zod and Typescript**](https://dev.to/varenya/making-impossible-states-impossible-ft-zod-and-typescript-4640)

You can go through it for more info.

So the initial UI user will see will be something like this (loading indicators..):


![Sample App Image 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0oygyvzhb95houbnli9.png)


For full fledged code for this approach:

[GitHub - varenya/swr-blog at pure-client-side](https://github.com/varenya/swr-blog/tree/pure-client-side)

## Prefetch on Server

As you can see from the above screenshot I have chosen `london` as the first location.

We could show the user a `loading` indicator and make users wait before they can see the weather info.

But why do it when we know that is the first thing user will see?

At the same time doesn’t make sense the get weather info for all cities on server either. Users would stare at a white screen longer than necessary.

So for this application we will only `fetch` weather info for `london` on server and will load the rest on the client.

In `next.js` to make a server side call we need to use the aptly named function `getServerSideProps` .

And to use it with `swr` we need to pass the data to `fallback` prop in the context that `swr` provides called `SWRConfig` .

So the `page` would look something like this:

```tsx
function Page(props) {
  return (
    <SWRConfig value={{ fallback: props.fallback }}>
      {/* Your App */}
      <Weather />
    </SWRConfig>
  );
}

// london data is prefetched!
export async function getServerSideProps(context: GetServerSidePropsContext) {
  const weatherResponse = await getWeatherInfo("london");
  return {
    props: {
      fallback: {
        london: weatherResponse,
      },
    },
  };
}
```

Full fledged code:

[GitHub - varenya/swr-blog at pure-server-side](https://github.com/varenya/swr-blog/tree/pure-server-side)

Let’s take a moment and talk about this in a bit more detail.

The demo app that I have created has few cities as options and we were able to leverage `SWR` and `Next.JS` to shift the remote call from client to server.

Now let’s take a step ahead here and assume that during a peak traffic time the weather API became slow so users would be staring at a white screen.

What if we had the ability to switch over to a pure client side solution in such a situation?

Because in the above case that would be a better option. Since instead of seeing a white screen users would see a loading indicator like in the `pure client side` approach.

Well for that we just need to remove the `getServerSideProps` call and we would be good.

So something like this:

```tsx
function Page(props) {
  return (
    <SWRConfig value={{ fallback: props.fallback }}>
      {/* Your App */}
      <Weather />
    </SWRConfig>
  );
}

// london data is prefetched!
export async function getServerSideProps(context: GetServerSidePropsContext) {
  // add a feature flag - what ever mechanism your org uses.
  const featureFlags = getFeatureFlags();

  if (!featureFlags.clientOnly) {
    const weatherResponse = await getWeatherInfo("london");
  }

  // sending an empty fallback if clientOnly flag is true
  return {
    props: {
      fallback: featureFlags.clientOnly
        ? {}
        : {
            london: weatherResponse,
          },
    },
  };
}
```

And et voila - we have shifted the call’s happening on the client!

This is quite handy - especially during a `pager duty` alert 😅 .

Also we are able to reuse lot of code here too:

```tsx
// on the server - to get weather data:
const weatherResponse = await getWeatherInfo("london");

// on the client
const { data, error } = useSWR(location, getWeatherInfo);
```

Since we are using same `getWeatherInfo` function we get type-safety across client and server.

There are plenty more things this enables for us:

- If the call fails with an `error` on server we can send out an empty `fallback` and client will try the call!
- We can set some timeout duration on server and throw an error and have the client make the call!
- Apply some retry options via `swr` on client when if it errors out or times out.

I will let you folks come up with creative solutions here, but I think you all got the gist.

## Prefetch on the client

Turns out we can do this via good old `link` tags on `html` :

```tsx
<link rel="preload" href="/api/weather/mumbai" as="fetch" crossorigin="anonymous">
```

Or like this :

```tsx
import useSWR, { preload } from "swr";

// ain't that cool!
preload("kolkata", getWeatherInfo);

function Weather() {
  // first param is the key and second argument is custom fetcher.
  // the key is passed to custom fetcher.
  const { data, error } = useSWR(location, getWeatherInfo);
  if (error) {
    return <p>Error!</p>;
  }
  if (!data) {
    return <p>Loading..</p>;
  }
  return (
    <>
      <p>{data.temperature}</p>
      <p>{data.humidity}</p>
    </>
  );
}
```

You will notice on the network tab that the `prefetch` query is executed first:


![Sample App Image 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dj97o023gvdye9z4vao7.png)

So if user selects `kolkata` chances of them seeing a indicator will be very less:

[https://weather-blog-gpeypw5gj-varen90.vercel.app/](https://weather-blog-gpeypw5gj-varen90.vercel.app/)

Full fledged code:

[GitHub - varenya/swr-blog at prefetch-client-side](https://github.com/varenya/swr-blog/tree/prefetch-client-side)

## Suspense

This is the other part of async coding that can get out of hand. Every time we make a remote call we have to deal with various states i.e.

- Trigger the async call
- Go into loading state
- If successful show data
- if error handle the error in a graceful way

Now these states have tendency to go out of hand. Even you even a few remote data dependencies the code complexity grows.

The above states can be modelled as a `union` to alleviate some of the pain:

```tsx
function WeatherLoader({ location }: WeatherProps) {
  const weatherData = useWeather(location);
  switch (weatherData.status) {
    case "error":
      return <WeatherErrror />;
    case "loading":
      return <WeatherContentLoader />;
    case "success":
      return <Weather weatherInfo={weatherData.data} />;
    default:
      const _exceptionCase: never = weatherData;
      return _exceptionCase;
  }
}
```

But this can get out of hand with multiple async data. In fact lot of `frontend` code will look like this 😃 .

Now bear in mind that we are using same `getWeatherInfo` so whatever error are thrown there will be handled as part of the “error” condition. We can tune the errors with some additional magic but I won’t go into detail for that here.

With `Suspense` the above can be refactored into some thing like this:

```tsx
function WeatherLoader({ location, retryLocation }: WeatherProps) {
  return (
    <ErrorBoundary
      FallbackComponent={WeatherError}
      resetKeys={[location]}
      onReset={() => retryLocation(location)}
    >
      <Suspense fallback={<WeatherContentLoader />}>
        <Weather location={location} />
      </Suspense>
    </ErrorBoundary>
  );
}
```

To get this working with `swr` all you need to do is:

```tsx
const { data } = useSWR<BasicWeatherInfo>(location, getWeatherInfo, {
  suspense: true,
});
```

Typically this would involve lot more `code` but that all is abstracted away for us when using libraries like this `swr` .

To be honest `suspense` is lot more than just a data fetching convenience. It literally `suspends` any logic that component is waiting for typically something asynchronous.

To accomplish this we actually have to `throw` a `promise` ! (`swr` does the heavy lifting for us here)

For now its not fully stable and has some gotchas -

[https://reactjs.org/blog/2022/03/29/react-v18.html#suspense-in-data-frameworks](https://reactjs.org/blog/2022/03/29/react-v18.html#suspense-in-data-frameworks)

Final Code using `Suspense` :

[GitHub - varenya/swr-blog](https://github.com/varenya/swr-blog)

Also using `react-query` :

[GitHub - varenya/swr-blog at use-react-query](https://github.com/varenya/swr-blog/tree/use-react-query)

And the final app link:

[https://weather-blog.vercel.app/](https://weather-blog.vercel.app/)

I hope you all found it useful. Thanks for reading!