Handling data fetching in Next.js with useSWR
Tags: nextjs, webdev
Published: 2023-07-26 15:37:26

**Written by [Elijah Agbonze](https://blog.logrocket.com/author/elijahtrillionz/)✏️**

When it comes to handling loading states, error handling, revalidating, prefetching, and managing multiple calls, using `useEffect` can be overwhelming. This has been the dilemma of client-side data fetching and handling where you either have to set up a state management system or settle for the `useEffect` Hook. 

But with [SWR](https://swr.vercel.app/), fetching and handling client-side data has never been easier. SWR, which stands for Stale-While-Revalidate, is a powerful library for data fetching, revalidating, and caching in React applications. The "Stale-While-Revalidate" concept means that while SWR is revalidating the data, it serves the stale data from the cache. This approach strikes the perfect balance between a responsive UI performance and ensuring your users always have access to up-to-date data. 

In this article, we are going to look at how we can use SWR to handle data with interactive examples. We will also take a look at some important features and supports of SWR. 

_Jump ahead:_

*   [Basics of the `useSWR` Hook](#basics-useswr-hook)
*   [Pagination with `useSWR`](#pagination-useswr)
*   [Reusable data fetching with SWR: Best practices](#reusable-data-fetching-swr-best-practices)
*   [Caching with SWR](#caching-swr)
*   [SSG/ISR/SSR support](#ssg-isr-ssr-support)
*   [Type safety with `useSWR`](#type-safety-useswr)
*   [Mutation and revalidation](#mutation-revalidation)
*   [Conditional fetching with `useSWR`](#conditional-fetching-useswr)
*   [Error handling strategies with `useSWR`](#error-handling-strategies-useswr)
*   [`useSWR` with GraphQL](#useswr-graphql)
*   [`useSWR` performance optimization](#useswr-performance-optimization)
*   [Testing strategies](#testing-strategies)

## Basics of the `useSWR` Hook

```javascript
const { data, isLoading, error } = useSWR(key, fetcher, options);
```

### `useSWR` params

The `useSWR` Hook accepts three parameters. The first two are required, and they determine what is going to be fetched and how it will be fetched. The third parameter is an object of options, which lets you tweak the way the data is handled. 

The `key` param represents the URL, in the case of REST APIs, to which the request is to be made. The reason for the strange `key` as name is because it actually is a key. It is a unique string and in some cases, can be a function, object, or an array. For example, during global mutations, the key of a `useSWR` Hook is used to make changes specific to that hook. 

The `fetcher` param is a function that returns a promise of the fetched data:

```javascript
const fetcher = (url) => fetch(url).then((res) => res.json());
```

The `fetcher` function is not limited to the Fetch API; you can also use Axios for REST APIs and the [graphql-request library](https://github.com/prisma-labs/graphql-request) for GraphQL. We will take a closer look at fetching GraphQL data later in this article:

```javascript
const { data } = useSWR(key, fetcher);
```

By default, the only argument passed into the `fetcher` function from the example above is the `key` param of SWR. 

Let's say we have more arguments to pass to the `fetcher` function. For example, passing a limit for fetching a list of limited comments from [JSONPlaceholder](https://jsonplaceholder.typicode.com), assuming the limit is triggered by the user on the app, may tempt us to do something like this:

```javascript
const fetcher = async (url, limit) => {
  const res = await fetch(`${url}?_limit=${limit}`);
};

const { data: comments } = useSWR(
  `https://jsonplaceholder.typicode.com/comments`,
  (url) => fetcher(url, limit)
);
```

This isn't accurate because the cached key, which is `https://jsonplaceholder.typicode.com/comments`, does not include `limit`, and if `limit` changes, SWR would still use the same cached key and return the wrong data. 

In cases like this, where passing multiple arguments to the `fetcher` function is paramount, you’d have to pass these arguments to the `key` param as either an array or an object:

```javascript
const fetcher = async ([url, limit]) => {
  const res = await fetch(`${url}?_limit=${limit}`);
};

const { data: comments } = useSWR(
  [`https://jsonplaceholder.typicode.com/comments`, limit],
  fetcher
);
```

In the example above, both `url` and `limit` are now part of the `key` param. Now, changes to `limit` will cause SWR to use a different cached key and provide the accurate data. The same thing applies with using an object. 

With the `options` param, you can make changes to the way your fetched data is being handled. This includes determining when your data should revalidate if need be, if there should there be a retry if an error occurred while fetching the data, what happens if it was successful, and what happens if it fails. Below are some of the `options` properties:

*   `revalidateOnFocus`: Automatically revalidates when a user refocuses on the window. Default is `true`
*   `onSuccess(data, key, config)`: Callback function when a request is successful, and if the request returns an error, the `onError` callback function can be used.
*   `shouldRetryOnError`: Turns on or off retrying when an error occurs. Default is `true`. If set to `true`, you can also determine the max retry count with the `errorRetryCount` option and the retry interval with the `errorRetryInterval` option.

Here is the [full list of `useSWR` `options` param](https://swr.vercel.app/docs/api#options).

### `useSWR` return values

The `useSWR` Hook returns five values:

```javascript
const { data, error, isLoading, isValidating, mutate } = useSWR(
  "/api/somewhere",
  fetcher
);
```

*   `data`: The data returned from the request to the given key. It returns `undefined` if not loaded
*   `error`: Any error thrown in the `fetcher` function. It could be a syntax, reference, or even error from the API. We will take a look at how we can throw errors later in this article
*   `isLoading`: Returns `true` /`false` if there's an ongoing request and no loaded data
*   `isValidating`: Returns `true`/`false` if there's an ongoing request or revalidation is loading. It is almost similar to `isLoading`. The only difference is that `isValidating` also triggers for previously loaded data
*   `mutate`: A function for making changes to the cached data specific to this cached key only

Let's take a look at an example that uses some of the things we've seen so far. In this simple example, we will fetch comments from JSONPlaceholder. First, create a new Next.js project and paste the code below in `pages.jsx`:

```javascript
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Home() {
  const {
    data: comments,
    isLoading,
    isError: error,
  } = useSWR(
    "https://jsonplaceholder.typicode.com/comments?_limit=6",
    fetcher,
    { revalidateOnFocus: false, revalidateOnReconnect: false }
  );

  if (error) {
    return <p>Failed to fetch</p>;
  }

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

  return (
       <ul>
          {comments.map((comment, index) => (
            <li key={index}>
              {comment.name}
            </li>
          ))}
        </ul>
  );
}
```

From the [example above](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main), notice we turned off revalidation. This is because we’re fetching an immutable data, so we can turn off revalidatio to prevent unnecessary request calls.

## Pagination with `useSWR`

SWR comes with built-in support for handling pagination easily. There are two common pagination UI: the numbered pagination UI and the infinite loading UI: ![Pagination With UseSWR](http://blog.logrocket.com/wp-content/uploads/2023/07/pagination-useswr.png) The numbered pagination UI is straightforward with `useSWR`:

```javascript
export default function Home() {
  const [pageIndex, setPageIndex] = useState(1);
  const { data: comments } = useSWR(
    `https://jsonplaceholder.typicode.com/comments?_page=${pageIndex}&_limit=6`,
    fetcher
  );
  return (
    <>
      {/** ... code for mapping through the comments **/}
      <button onClick={() => setPageIndex((_currentPage) => _currentPage - 1)}>
        Prev
      </button>
      <button onClick={() => setPageIndex((_currentPage) => _currentPage + 1)}>
        Next
      </button>
    </>
  );
}
```

As seen above, the `pageIndex` state changes the cached key when it is changed. This will cause SWR to request for new data from the API. Here is a [CodeSandbox to try out this example](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/fetching/numbered-pagination-comments.js:1,1). 

With SWR's cache, we can preload the next page. All we have to do is create an abstraction for the next page and render it in a hidden div. This way the next page is already loaded and ready to be displayed:

```javascript
const Page = ({ index }) => {
  const { data: comments } = useSWR(
    `https://jsonplaceholder.typicode.com/comments?_page=${index}&_limit=6`,
    fetcher
  );
  return (
    <ul>
      {comments.map((comment, index) => (
        <li key={index}>{comment.name}</li>
      ))}
    </ul>
  );
};
export default function Home() {
  const [pageIndex, setPageIndex] = useState(1);
  return (
    <>
      <Page index={pageIndex} /> {/** for the current displaying page **/}
      <div className='hidden'>
        <Page index={pageIndex + 1} />
      </div>{' '}
      {/** for the next page**/}
      <div>
        <button
          onClick={() => setPageIndex((_currentPage) => _currentPage - 1)}
        >
          Prev
        </button>
        <button
          onClick={() => setPageIndex((_currentPage) => _currentPage + 1)}
        >
          Next
        </button>
      </div>
    </>
  );
}
```

SWR also provides a different [hook for handling infinite loading](https://swr.vercel.app/docs/pagination#useswrinfinite):

```javascript
import useSWRInfinite from 'swr/infinite';
const getKey = (pageIndex, previousPageData) => {
  // return a falsy value if this is the last page
  if (pageIndex && !previousPageData.length) return null;
  return `https://jsonplaceholder.typicode.com/comments?_page=${pageIndex}&_limit=6`;
};
export default function Home() {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher);
  return (
    <>
      <ul>
        {data.map((comments) => {
          return comments.map((comment, index) => (
            <li key={index}>{comment.name}</li>
          ));
        })}
      </ul>
      <button onClick={() => setSize(size + 1)}>Load more</button>
    </>
  );
}
```

Here is this [example in CodeSandbox](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/fetching/infinite-loading-comments.js:1,1). Similar to the `useSWR` Hook, `useSWRInfinite` takes in a `getKey` function that returns the request key, as well as a `fetcher` function. It returns an extra `size` value along with a function `setSize` to update the `size` value. 

By default, the `useSWRInfinite` Hook fetches data for each page in sequence. This means the key for the next page depends on the previous one. This is often useful when the data for fetching the next page depends on the current one. 

However, if you don't want to fetch the pages sequentially, then you can turn it off by setting the `parallel` option of `useSWRInfinite` to `true`:

```javascript
const { data, size, setSize } = useSWRInfinite(getKey, fetcher, {
  parallel: true,
});
```

## Reusable data fetching with SWR: Best practices

When fetching similar data across your application, it is best to make fetching the data reusable to avoid duplicate code, and have better code efficiency and maintainability. Assuming we have a dashboard where the current user is displayed across multiple components in our UI, usually you'd want to fetch the user in the top-level component and pass it down to all components using props or [React Context](https://reactjs.org/docs/context.html). But with SWR, it becomes easier and more efficient:

```javascript
const useUser = () => {
  const { data, isLoading, error } = useSWR(`/api/user`, fetcher);

  return { user: data, isLoading, error };
};
```

Now we can use the hook above across multiple components:

```javascript
const Home = () => {
  const { user } = useUser();

  return (
    <header>
      <h2>Welcome back {user.username}</h2>
      <NavBar />
    </header>
  );
};

const UpdateProfile = () => {
  const { user } = useUser();

  return (
    <header>
      <h2>Updating {user.username}</h2>
      <NavBar />
    </header>
  );
};

const NavBar = () => {
  const { user } = useUser();

  return (
    <div>
      <img src={user.avatar} /> {user.username};
    </div>
  );
};
```

It doesn't matter how many times you reuse the `useUser` Hook, the request will only be sent once. This is because all instances of the hook use the same key, so the request is automatically cached and shared. 

Reusable data fetching with SWR also helps fetch data in the declarative way, which means you only need to specify what data is used by the component.

## Caching with SWR

By default, SWR uses a global cache to store and share data across all your components. This global cache is an instance object that is only created when the app is initialized, and destroyed when the app exits. This is why it is primarily used for short-term caching and fast data retrieval. 

If the default cache of SWR is not satisfying for your app, you can make use of a custom cache provider like [window's localStorage property](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), or [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). This will help you store larger amounts of data and persist it across page reloads. This means exiting the app/browser will not destroy the stored data. 

A cache provider is map-like, which means you can directly use the [JavaScript Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) instance as the cache provider for SWR. You can create custom cache providers on the `SWRConfig` component:

```javascript
import { SWRConfig } from "swr";

const App = () => {
  return (
    <SWRConfig value={{ provider: () => new Map() }}>{/** app **/}</SWRConfig>
  );
};
```

The custom provider defined above is similar to the default provider of SWR. All SWR hooks under the `SWRConfig` component will make use of the defined provider. 

You can access the current cache provider of a component with the `useSWRConfig` Hook. Along with the `cache` property is the `mutate` property for modifying the cache directly:

```javascript
import { useSWRConfig } from "swr";

const App = () => {
  const { data: user } = useSWR("/api/user");
  const { cache, mutate } = useSWRConfig();

  useEffect(() => {
    console.log(cache);
  }, [cache]);

  const updateName = async () => {
    const newName = user.name.toUpperCase();
    await updateNameInDB(newName);
    mutate("/api/user", { ...user, name: newName });
  };

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={updateName}>Update name</button>
    </div>
  );
};
```

You should never update the default cache directly. Instead, use the `mutate` function to make updates to the cache directly. Now let's take a look at using a more persistent cache storage using `localStorage`. 

SWR has a [set of standard types](https://swr.vercel.app/docs/advanced/cache#cache-provider) for creating a custom cache provider (i.e, the `get`, `set`, `delete`, and `keys` methods):

```javascript
const cacheGet = (key) => {
  const cachedData = localStorage.getItem(key);
  if (cachedData) {
    return JSON.parse(cachedData);
  }
  return null;
};

const cacheSet = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value));
};

const cacheDelete = (key) => {
  localStorage.removeItem(key);
};

const cacheKeys = () => {
  const keys = [];
  for (let i = 0; i < localStorage.length; i++) {
    keys.push(localStorage.key(i));
  }
  return keys;
};

export { cacheGet, cacheSet, cacheDelete, cacheKeys };
```

Now we can define each of these methods in the `provider` function in `SWRConfig`:

```javascript
const App = () => {
  return (
    <SWRConfig
      value={{
        provider: () => ({
          get: cacheGet,
          set: cacheSet,
          keys: cacheKeys,
          delete: cacheDelete,
        }),
      }}
    >
      {/** app **/}
    </SWRConfig>
  );
};
```

Any `useSWR` request within the `SWRConfig` above will be stored in the `localStorage`. This means you can access your cached data after reloading, and even when offline. 

If you don't want to define all the methods for your project, and want something similar to the default cache while still storing in a more persistent memory like `localStorage`, all you have to do is sync your cache with `localStorage`, and you’ll still be able to access it offline:

```javascript
const localStorageProvider = () => {
  const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));

  console.log(map);
  window.addEventListener("beforeunload", () => {
    const appCache = JSON.stringify(Array.from(map.entries()));
    localStorage.setItem("app-cache", appCache);
  });

  return map;
};

const App = () => {
  return (
    <SWRConfig
      value={{
        provider: localStorageProvider,
      }}
    >
      {/** app **/}
    </SWRConfig>
  );
};
```

Notice we're still making use of the JavaScript `Map` instance, so we don't have to define the standard methods for an SWR cache provider.

## SSG/ISR/SSR support

Having a statically generated blog improves search engine performance and load times. Despite pre-rendering, client-side data fetching is still essential for real-time views, comments, authentication, and data revalidation. 

Let’s take a look at pre-rendering in Next.js, which can be done with SSG, ISR, or SSR, and fetching client-side data with SWR. 

[SSG (Static Site Generation)](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) allows you to generate static pages at build time. Below is an example of a pre-rendered blog post where SWR is used to revalidate the number of views:

```javascript
const Post = () => {
  const { data: post, mutate } = useSWR(
    "/api/post",
    (url) => fetch(url).then((res) => res.json()),
    { refreshInterval: 1000 }
  );
  return (
    <>
          <p>
            Views: {post.views}
          </p>
        <button
          onClick={async () => {
            await fetch("/api/post", {
              method: "put",
            });
            mutate();
          }}
        >
          Increment views
        </button>
    </>
  );
};
const Index = ({ fallback }) => {
  return (
    <SWRConfig value={{ fallback }}>
      <Post />
    </SWRConfig>
  );
};
export default Index;

export async function getStaticProps() {
  const res = await fetch(`/api/post`);
  const post = await res.json();
  return {
    props: {
      fallback: {
        "/api/post": post,
      },
    },
  };
}
```

![SSG With UseSWR](http://blog.logrocket.com/wp-content/uploads/2023/07/ssg-useswr.gif) In the [example above](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=%2F.codesandbox%2Ftasks.json%3A1%2C1), I made use of `refreshInterval` to tell `useSWR` how often it should revalidate. 

[ISR (Incremental Static Regeneration)](https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration) is a extension of SSG. It provides a `revalidate` property that is used to specify how often (in seconds) the page should be regenerated. All we have to do is simply add `revalidate` to the returned value from `getStaticProp`:

```javascript
export async function getStaticProps() {
  const res = await fetch(`/api/post`);
  const post = await res.json();

  return {
    props: {
      fallback: {
        "api/post": post,
      },
    },
    revalidate: 10,
  };
}
```

We also had to remove the `refreshInterval` prop from the SSG example because ISR will incrementally regenerate this page at runtime based on the `revalidate` property and when it is requested. So whenever a new user visits the post page, they will have access to the updated views. <span class="attrlink url thread-570138671662700775082468 attrcomment attrcommentfirst thread-570138671662700775082468-first author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qowz80ziz71z988z78zqz66zxrcah3z122zz68zz72ztpbz68zcz84zz76zmabz65zef" role="mark" aria-details="comment-thread-570138671662700775082468"><span class="comment-extra-inner-span">View the [full code and demo of this example](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=%2F.codesandbox%2Ftasks.json%3A1%2C1)</span></span><span class="thread-570138671662700775082468 attrcomment author-d-1gg9uz65z1iz85zgdz68zmqkz84zo2qowz81zz86zr286jz65zz122zz65zspgz90zfiz78zg9z76zz67zz81zz79zqdgz85z75z74zz122z"><span class="comment-extra-inner-span">.</span></span> 

[SSR (Server Side Rendering)](https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering) generates a page on the server side at runtime. This means that the contents of the page are always up-to-date with the latest data because it is generated at the time of the request. If we were to adjust the examples above to SSR, we'd have this:

```javascript
export default function Index({ post: initialPost }) {
  const { data: post } = useSWR(
    '/api/post',
    (url) => fetch(url).then((res) => res.json()),
    { fallbackData: { initialPost } }
  );
  return (
    <>
      <h2>{post.title}</h2>
    </>
  );
}
export async function getServerSideProps() {
  const res = await fetch(`/api/post`);
  const post = await res.json();
  return {
    props: {
      post,
    },
  };
}
```

`initialPost` is the post fetched on the server side with `getServerSideProps`. When the page is requested, it will render the up-to-date views. You can turn all revalidation from SWR off to see the dynamic rendering at work. [View the demo of this example](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=%2F.codesandbox%2Ftasks.json%3A1%2C1).

## Type safety with useSWR

SWR supports TypeScript out of the box. Recall that when specifying multiple arguments for the `fetcher` function, we usually pass it to the `key` param. Now for specifying the types of these arguments, SWR will infer the types of `fetcher` from `key`:

```javascript
// url will be inferred as a string, while limit will be inferred as number
const { data: comments } = useSWR(
  [`https://jsonplaceholder.typicode.com/comments`, 2],
  ([url, limit]) => fetch(`${url}?_limit=${limit}`).then((res) => res.json())
);
```

If you're not happy with inferred types, you can explicitly specify the types you want:

```javascript
import useSWR, { Fetcher } from "swr";

const fetcher: Fetcher<Comment, string> = fetch(url).then((res) => res.json());

const { data: comments } = useSWR(
  `https://jsonplaceholder.typicode.com/comments`,
  fetcher
);
```

## Mutation and revalidation

Mutation is simply for updating the data of a cached key, while revalidating whenever/however triggered re-fetches a cached key. 

There are two ways of using mutation. The first way is by using the global `mutate` function, which can mutate any cached key:

```javascript
import { useSWRConfig } from "swr";

const App = () => {
  const { data: user } = useSWR("/api/user");
  const { cache, mutate } = useSWRConfig();

  const updateName = async () => {
    const newName = user.name.toUpperCase();
    await updateNameInDB(newName);
    mutate("/api/user", { ...user, name: newName });
  };

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={updateName}>Update name</button>
    </div>
  );
};
```

`mutate` in the example above is a global mutate because it's from the `useSWRConfig`. We could also import the global `mutate` from `swr`:

```javascript
import { mutate } from "swr";

const App = () => {
  const { data: user } = useSWR("/api/user");

  const updateName = async () => {
    const newName = user.name.toUpperCase();
    await updateNameInDB(newName);
    mutate("/api/user", { ...user, name: newName });
  };

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={updateName}>Update name</button>
    </div>
  );
};
```

The first param is the `key` of the `useSWR` Hook you want to mutate. The second is for passing the updated data. 

The second way of using mutation is the bound `mutate`, which only mutates its corresponding `useSWR` Hook:

```javascript
const App = () => {
  const { data: user, mutate } = useSWR("/api/user");

  const updateName = async () => {
    const newName = user.name.toUpperCase();
    await updateNameInDB(newName);
    mutate({ ...user, name: newName }); // no need to specify the key, as the key is already known
  };

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={updateName}>Update name</button>
    </div>
  );
};
```

**Revalidation** 

Calling `mutate` with `key` will trigger a revalidation for all `useSWR` hooks that uses that key. But without a `key`, it will only revalidate its corresponding `useSWR` Hook:

```javascript
const App = () => {
  const { data: user, mutate } = useSWR("/api/user");

  const updateName = async () => {
    const newName = user.name.toUpperCase();
    await updateNameInDB(newName);
    mutate();
  };

  return (
    <div>
      <p>{user.name}</p>
      <button onClick={updateName}>Update name</button>
    </div>
  );
};
```

In the example above, we didn't have to specify a cached data because the `mutate` function will trigger a re-fetch and that will cause the updated data to be displayed and cached. While this sounds cool, it can be a problem when you're re-fetching a large amount of data when you only wanted the new data to be added. Here is an [example](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=%2F.codesandbox%2Ftasks.json%3A1%2C1). 

In cases where the API you're writing to returns the updated/new value, we can simply update the cache after mutation:

```javascript
const addPost = async () => {
  if (!postTitle || !postBody) return;

  const addNewPost = async () => {
    const res = await fetch("/api/post/new", {
      method: "post",
      headers: {
        "content-type": "application/json",
        accept: "application/json",
      },
      body: JSON.stringify({
        title: postTitle,
        body: postBody,
        id: posts.length + 1,
      }),
    });

    return await res.json();
  };
  mutate(addNewPost, {
    populateCache: (newPost, posts) => {
      return [...posts, newPost];
    },
    revalidate: false,
  });
};
```

Here is the CodeSandbox for the [complete example](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/mutations/posts.js:1,1). In the example above, notice that we changed the `revalidate` property to `false`. This will prevent the SWR from re-fetching the whole data when `mutate` is used. 

Like what we've seen above, revalidating can sometimes be redundant and unnecessary. To stop these unnecessary requests, you can turn off all automatic revalidations with `useSWRImmutable`:

```javascript
import useSWRImmutable from "swr/immutable";

const App = () => {
  const { data: user, mutate } = useSWRImmutable("/api/user");

  return (
    <div>
      <p>{user.name}</p>
    </div>
  );
};
```

It's the same as `useSWR`, with the only difference being that all automatic revalidation options such as `revalidateIfStale`, `revalidateOnFocus`, and `revalidateOnReconnect` are set to `false` automatically.

## Conditional fetching with `useSWR`

The `key` param can also be a function, as long as it returns a string, array, object, or a falsy value. When fetching data that depends on another, you can either pass a function to the `key` param or simply use a ternary operator directly, like so:

```javascript
const fetcher = (url) => fetch(url).then((res) => res.json());

const { data: posts } = useSWR(
  "https://jsonplaceholder.typicode.com/posts?_limit=20",
  fetcher
);

// using a ternary operator directly
const { data: comments } = useSWR(
  posts.length >= 1
    ? `https://jsonplaceholder.typicode.com/posts/${posts[12].id}/comments?_limit=6`
    : null,
  fetcher
);

// using a function
const { data: comments } = useSWR(
  () =>
    posts.length >= 1
      ? `https://jsonplaceholder.typicode.com/posts/${posts[12].id}/comments?_limit=6`
      : null,
  fetcher
);
```

Here is the [example above in CodeSandbox](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/fetching/conditional-comments.js:1,1). When a falsy value is passed to the `key` param, it will return as an error by the `useSWR` Hook.

## Error handling strategies with `useSWR`

As a developer, when an error is thrown when fetching data, you can either decide to display the error, or try fetching again. Let’s take a look at handling these options:

### Displaying errors

One benefit of using `useSWR` is that it returns errors specific to a component, and right there you can display what went wrong. We've seen example of this throughout this article, now let’s see how we can send customized messages based on the error:

```javascript
const fetcher = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    const errorRes = await res.json();

    const error = new Error();
    error.info = errorRes;
    error.status = res.status;
    error.message = "An error occurred while fetching data";
    throw error;
  }
  return await res.json();
};
```

Here is the complete [example on CodeSandbox](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/fetching/error-comments.js:1,1). You can also display an error globally with the [`SWRConfig` component](https://swr.vercel.app/docs/error-handling#global-error-report). 

**Retrying on error** 

SWR provides an `onErrorRetry` callback function as part of the `useSWR` options. It uses the [exponential backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) to retry the request exponentially on error. This prevents incessant retrying. The callback allows you to control retrying errors based on conditions:

```javascript

useSWR("/api/user", fetcher, {
  onErrorRetry: (error, key, config, { retryCount }) => {
    // Never retry on 404.
    if (error.status === 404) return;

    // Only retry up to 10 times.
    if (retryCount >= 10) return;
  },
});
```

## `useSWR` with GraphQL

Fetching data from a GraphQL API with `useSWR` is similar to fetching from a REST API. The only difference is that you need a third-party library to make a request to the GraphQL API. A popular library is the [graphql-request](https://www.npmjs.com/package/graphql-request) package:

```javascript
import request from 'graphql-request';
import useSWR from 'swr';
const graphQLFetcher = (query) => request('/graphql', query);
const Books = () => {
  const {
    data,
    error: isError,
    loading: isLoading,
  } = useSWR(
    `
      {
        books {
          name
          id
        }
      }
    `,
    graphQLFetcher
  );
  if (isError) return <div>Error loading books</div>;
  if (isLoading) return <div>Loading books...</div>;
  return (
    <ul>
      {data?.books.map((book) => (
        <li key={book.id}>{book.name}</li>
      ))}
    </ul>
  );
};
export default Books;
```

Here is the [demo of the example](https://29dhwh-3000.csb.app/fetching/graphql-books) above. 

Mutating a GraphQL API is the same thing as what we've seen before. You can either use the global `mutate`, bound `mutate`, or the `useSWRMutation` Hook. Here is an [example with bound `mutate`](https://codesandbox.io/p/github/Elijah-trillionz/swr-features-app/main?file=/pages/mutations/graphql-books.js:1,1). 

One useful feature of `useSWR` that we haven’t discussed yet is the `useSWRSubscription` Hook. This hook is used for subscribing to real-time data. Let's say we have a WebSocket server that sends a new message every second:

```javascript
// the server
const WebSocket = require("ws");

const wss = new WebSocket.Server({ port: 5000 });

wss.on("connection", (ws) => {
  console.log("Client connected");

  let number = 5000;
  setInterval(() => {
    number -= 1;
    ws.send(`Current number is ${number}`);
  }, 1000);

  ws.on("close", () => {
    console.log("Client disconnected");
  });
});

console.log("WebSocket server started on port 5000");
```

Making use of the `useSWRSubscription`, we can subscribe to this server and listen for events to the client like so:

```javascript
import useSWRSubscription from "swr/subscription";

const Countdown = () => {
  const { data } = useSWRSubscription(
    "ws://localhost:5000",
    (key, { next }) => {
      const socket = new WebSocket(key);
      socket.addEventListener("message", (event) => next(null, event.data));
      socket.addEventListener("error", (event) => next(event.error));
      return () => socket.close();
    }
  );

  return (
    <>
      <p>{data}</p>
    </>
  );
};

export default Countdown;
```

## `useSWR` performance optimization

*   **Caching strategies**: `useSWR` allows you to specify caching strategies for fetching data. For example, you can use the "cache-first" strategy to serve data from the cache, thereby making a network request only if data is not available
*   **Prefetching:** The `useSWR` `mutate` function can be used for data prefetching. The `mutate` function allows you to update the cached data without making a new network request, allowing you to pre-fetch data and keep it up-to-date in the cache
*   **Using custom hooks**: Encapsulate `useSWR` logic in custom hooks to promote reusable data fetching and make it easier to manage data fetching across your application
*   **Error handling**: Display appropriate error messages to users or automatically retry errors
*   **Declarative fetching**: `useSWR` makes it easier to fetch only the data that the current component requires, rather than fetching all data simultaneously. This helps minimize unnecessary network calls
*   **Loading states**: Provide loading state feedback to users while data is being fetched

## Testing strategies <a name="testing-strategies">

Testing `useSWR` can seem daunting at first, but it's an essential step to ensure your React applications work smoothly. Some of these strategies include:

*   Testing data fetching by crafting mock API responses and ensuring your component displays the fetched data as expected
*   Testing that the caching behavior works as intended
*   Testing mock API responses with errors to ensure your component displays appropriate error messages
*   Testing loading states by simulating data fetch delays
*   Testing that data is revalidated and updated as expected, especially after the revalidation interval.
*   Testing data prefetching using the `mutate` function from `useSWR`. Verify that the data is indeed prefetched and readily available in the cache when needed

## Conclusion

So far, we've seen how easy it is to fetch, prefetch, cache, mutate, and revalidate data regardless of the size with `useSWR`. We also looked at how to fetch from two different kinds of API (REST and GraphQL) using custom fetcher functions for them. 

Each feature that we discussed is important and can be useful in your day to day client-side data fetching. There are many other features you can check out on the [official `useSWR` docs](https://swr.vercel.app/docs/getting-started). Happy hacking!

---

## [LogRocket](https://lp.logrocket.com/blg/nextjs-signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket](https://lp.logrocket.com/blg/nextjs-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/nextjs-signup)

[LogRocket](https://lp.logrocket.com/blg/nextjs-signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — [start monitoring for free](https://lp.logrocket.com/blg/nextjs-signup).