DEV Community

Cover image for Handling data fetching in Next.js with useSWR
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Handling data fetching in Next.js with useSWR

Written by Elijah Agbonze✏️

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, 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

const { data, isLoading, error } = useSWR(key, fetcher, options);
Enter fullscreen mode Exit fullscreen mode

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:

const fetcher = (url) => fetch(url).then((res) => res.json());
Enter fullscreen mode Exit fullscreen mode

The fetcher function is not limited to the Fetch API; you can also use Axios for REST APIs and the graphql-request library for GraphQL. We will take a closer look at fetching GraphQL data later in this article:

const { data } = useSWR(key, fetcher);
Enter fullscreen mode Exit fullscreen mode

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, assuming the limit is triggered by the user on the app, may tempt us to do something like this:

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)
);
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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.

useSWR return values

The useSWR Hook returns five values:

const { data, error, isLoading, isValidating, mutate } = useSWR(
  "/api/somewhere",
  fetcher
);
Enter fullscreen mode Exit fullscreen mode
  • 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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

From the example above, 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 The numbered pagination UI is straightforward with useSWR:

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

SWR also provides a different hook for handling infinite loading:

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here is this example in CodeSandbox. 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:

const { data, size, setSize } = useSWRInfinite(getKey, fetcher, {
  parallel: true,
});
Enter fullscreen mode Exit fullscreen mode

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. But with SWR, it becomes easier and more efficient:

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

  return { user: data, isLoading, error };
};
Enter fullscreen mode Exit fullscreen mode

Now we can use the hook above across multiple components:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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, or IndexedDB. 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 instance as the cache provider for SWR. You can create custom cache providers on the SWRConfig component:

import { SWRConfig } from "swr";

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

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 for creating a custom cache provider (i.e, the get, set, delete, and keys methods):

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 };
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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) 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:

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,
      },
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

SSG With UseSWR In the example above, I made use of refreshInterval to tell useSWR how often it should revalidate.

ISR (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:

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

  return {
    props: {
      fallback: {
        "api/post": post,
      },
    },
    revalidate: 10,
  };
}
Enter fullscreen mode Exit fullscreen mode

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. View the full code and demo of this example.

SSR (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:

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,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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.

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:

// 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())
);
Enter fullscreen mode Exit fullscreen mode

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

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
);
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

Here is the CodeSandbox for the complete example. 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:

import useSWRImmutable from "swr/immutable";

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

  return (
    <div>
      <p>{user.name}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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
);
Enter fullscreen mode Exit fullscreen mode

Here is the example above in CodeSandbox. 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:

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();
};
Enter fullscreen mode Exit fullscreen mode

Here is the complete example on CodeSandbox. You can also display an error globally with the SWRConfig component.

Retrying on error

SWR provides an onErrorRetry callback function as part of the useSWR options. It uses the exponential backoff algorithm to retry the request exponentially on error. This prevents incessant retrying. The callback allows you to control retrying errors based on conditions:


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;
  },
});
Enter fullscreen mode Exit fullscreen mode

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 package:

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;
Enter fullscreen mode Exit fullscreen mode

Here is the demo of the example 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.

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:

// 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");
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

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

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. Happy hacking!


LogRocket: 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.

LogRocket Signup

LogRocket 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.

Top comments (1)

Collapse
 
femi_akinyemi profile image
Femi Akinyemi

Thanks for sharing!