Handling Data Fetching in Next.js with useSWR

Master data fetching in Next.js with useSWR. Learn caching, pagination, error handling, and more.

September 19, 2023

Dealing with handling loading states, revalidating, prefetching, error handling and managing multiple calls using [useEffect](https://legacy.reactjs.org/docs/hooks-effect.html) can be tedious. This has been a controversial discussion when it comes to client-side data fetching and handling, where you either have to set up a state management system or use useEffect Hook.

SWR is a powerful utility library for data fetching, revalidating, and caching in React applications. With SWR, fetching and handling client-side data has become easier with simple code scripts. SWR, which stands for “Stale-While-Regenerate,” means that while SWR is revalidating the data, it serves the state data from the cache. SWR data fetching strikes a perfect balance between a responsive UI performance and ensuring users always have access to up-to-date data.

This article will focus mainly on how to use SWR to handle data, important features and supports of SWR. Use the on-page links below to navigate quickly to the different sections of the article:

Installation

In a Next.js application, using the npm install swr command is not required, but it can be beneficial depending on your specific use cases. Inside of your React project directory, run the following command.

NPM: npm install swr
YARN:  yarn add swr
PNPM: pnpm add swr

Basics of useSWR

useSWR Parameters

The useSWR Hook accepts three parameters which the first two are required. They determine what is going to be fetched. The third parameter is an object of options which provides you with the flexibility to tweek how the data is handled.

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

For normal RESTful APIs with JSON data, you need to create a fetcher function, which is basically a wrapper for the native [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).

const fetcher = (…args) ⇒ fetch(…args).then(resres.json())

The key param in useSWR stands for the URL, in the case of REST APIs to which the request is made to. The reason for naming this as ‘key’ is because it is actually a key; it is a unique string and, in some cases, can be a function, object, or an array.

The fetcher param, as declared above, is a function which returns a promise of the fetched data. This function is not limited to fetch API, you can also use Axios for REST APIs and the [graphql-request-library](https://github.com/jasonkuhrt/graphql-request) for GraphQl.

The fetcher function can take more than one argument but by default, it takes only one. Let’s say we have a scenario where we are fetching data from JSONPlaceholder and we are assuming the limit is triggered by user interaction on a web application, and the key, which in this case is the JSONPlaceholder URL, contains limit query. The URL and the limit now part of the key param, on any change to the limit, will cause the SWR to use a different cached key and provide accurate data. The same instance applies to key as an object or an array.

The illustration above is a good example of the fetcher function taking more than one argument, which would, in this case, be paramount. The key param also would have to take this argument as an array or an object.

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

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

// with single argument
const fetcher = async (url) => {
  const res = await fetch(`${url}`);
};

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

With the option param, changes can be made to the way fetched data is being handled. Determining the way data is revalidated, if there should be a retry if an error occurred while fetching the data, and what happened on successful or failed. Options properties are defined below:

  • revalidateOnFocus: When the user refocuses a page or switches between tabs, SWR automatically revalidates data. This can be useful to immediately synchronize to the latest state. It is also helpful for refreshing data in scenarios like stale mobile tabs or laptops that went to sleep. This option is enabled by default and can be disabled by setting to false.
  • refreshInterval: Data changes because of multiple devices, multiple users and multiple tabs. SWR will give you the option to automatically re-fetch data. Re-fetching will only happen if the component associated with the hook is on screen. This can be enabled by setting the value. The value for refreshIntervalis numerical in seconds.
  • refreshWhenHidden: When this is set to true, SWR will fetch when the webpage is not on screen.
  • refreshWhenOffline: SWR is smart enough to fetch when there is no network connection when this option is set to true.
  • revalidateOnReconnect: It is useful to also revalidate when a user is back online. We have this scenario happen a lot when a user unlocks their computer, but the internet is not yet connected at the same moment.
  • onSuccess( data, key, config ): This option type is the callback function when a request is successful. If the request returns an error, the onError callback function can be used in the instance.
  • shouldRetryOnError: The default value for this option type is true. It indicates if a retry should happen when an error occurs. You can also determine the maximum retry count with the errorRetryCountoption and retry interval with errorRetryInterval option.

Since version 1.0, SWR provides a helper hook called useSWRImmutable to mark the resource as immutable.

Usage

**import useSWRImmutable from ‘swr/immutable’;

// ...

useSWRImmutable( key, fetcher, options );**

The useSWRImmutable Hook has the same API interface as the normal useSWR Hook.

uswSWR( key, fetcher, {
   revalidateIfState: false,
   revalidateOnFocus: false,
   revalidateOnReconnect: false,
} )

// same as 

useSWRImmutable( key, fetcher );

The revalidateOnMount Hook is useful to force override SWR revalidation on mounting. The value by default is set to undefined.

SWR Mount Flow 

First of all, SWR checks if revalidateOnMount is defined. If it is true, it starts the request and stops if it is false. revalidateIfStale is useful to control the mount behaviour, and it is set to true by default. It only re-fetches if there is any cache data, or else it will not re-fetch.

Handy Tips: useSWR Return Values

const { data, error, isLoading, isValidating, mutate } = useSWR(
  "/your/api",
  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 an 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

When the data we are working with is immutable data, the option of turning off revalidation can be utilized. So we can turn off revalidation to prevent unnecessary request calls.

An example of this scenario is:

export default function Home() {
  const {
    data,
    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...</p>;
  }

  return (
       <ul>
          {data.map((datum, index) => (
            <li key={index}>
              {datum.name}
            </li>
          ))}
        </ul>
  );
}

Reusable Data Fetching With SWR

There may be situations where data needs to be reused in many places of the UI. SWR makes creating reusable data hooks on top of it possible and easy.

Before the use of Hooks came, we usually needed to keep all the data fetching in the top-level component and add props to every component deep down the tree. The code will become harder to maintain if we add more data dependency to the page. And also, a more complex situation (prop drilling) is created.

Although we can avoid passing props using Context, there's still the dynamic content problem: components inside the page content can be dynamic, and the top-level component might not know what data will be needed by its child components.

SWR solves the problem perfectly:

function useUser(id) {
  const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading,
    isError: error
  }
}
function Page () {
  return <div>
    <Navbar />
    <Content />
  </div>
}

// child components

function Navbar () {
  return <div>
    ...
    <Avatar />
  </div>
}

function Content () {
  // Let's assumme id is coming from the url
  const { user, isLoading } = useUser(id)
  if (isLoading) return <Spinner />

  return <h1>Welcome back, {user.name}</h1>
}

function Avatar () {
  // Let's assumme id is coming from the url
  const { user, isLoading } = useUser(id)
  if (isLoading) return <Spinner />

  return <img src={user.avatar} alt={user.name} />
}

In the example above, data is bound to the component which needs the data, making all components independent of each other. Higher Order Component / parent components don’t need to know anything about the data or passing data around. The code is much simpler and easier to maintain now.

The most beautiful thing is that there will be only 1 request sent to the API because they use the same SWR key, and the request is deduped, cached and shared automatically. Also, the application now has the ability to re-fetch the data on `user focus or network reconnect!` That means when the user's laptop wakes from sleep or they switch between browser tabs, the data will be refreshed automatically.

Caching With useSWR

Caching is available in version ≥ 1.0.0

By default, SWR uses a global cache to store and share data across all 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.

Cache Provider

The cache provider intends to enable SWR with more customized storage. The cache provider is a Map-like object which matches the following Typescript definition and can be imported from SWR.

interface Cache<Data> {
  get(key: string): Data | undefined
  set(key: string, value: Data): void
  delete(key: string): void
  keys(): IterableIterator<string>
}

Create Cache Provider

You can directly use the Javascript Map instance as the cache provider for SWR.

The provider option of SWRConfig receives a function that returns a cache provider. The provider will then be used by all SWR hooks inside that SWRConfig boundary.

import useSWR, { SWRConfig } from 'swr'

function App() {
  return (
    <SWRConfig value={{ provider: () => new Map() }}>
      <Component/>
    </SWRConfig>
  )
}

All SWR hooks inside <Component/> will read and write from that Map instance. In the example above, when the <App/> component is re-mounted, the provider will also be re-created. Cache providers should be put higher in the component tree or outside of render.

When nested, SWR hooks will use the upper-level cache provider. If there is no upper-level cache provider, it fallbacks to the default cache provider, which is an empty Map.

Handling Data Fetching in Next.js with useSWR

Access Current Cache Provider

When inside a React component, you need to use the [useSWRConfig](https://swr.vercel.app/docs/global-configuration#access-to-global-configurations) hook to get access to the current cache provider as well as other configurations including mutate:

import { useSWRConfig } from 'swr'

function SomeComponent() {
  const { cache, mutate, ...extraConfig } = useSWRConfig()
  // ...
}

If it's not under any <SWRConfig>, it will return the default configurations.

The default cache should never be updated directly; use the mutate function to make updates to the cache directly.

Syncing Cache to localStorage for Persistent Cache

function localStorageProvider() {
  // When initializing, we restore the data from `localStorage` into a map.
  const map = new Map(JSON.parse(localStorage.getItem('app-cache') || '[]'))

  // Before unloading the app, we write back all the data into `localStorage`.
  window.addEventListener('beforeunload', () => {
    const appCache = JSON.stringify(Array.from([map.entries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries)()))
    localStorage.setItem('app-cache', appCache)
  })

  // We still use the map for write & read for performance.
  return map
}

Then, use it as a provider:

<SWRConfig value={{ provider: localStorageProvider }}>
  <App/>
</SWRConfig>

Pagination With useSWR

SWR comes with built-in support for pagination. There are two common pagination UI: the number pagination UI and the infinite loading UI.

Pagination

Handling Data Fetching in Next.js with useSWR

Let’s see how we can implement number pagination UI with useSWR:

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  // The API URL includes the page index, which is a React state.
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);

  // ... handle loading and error states

  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

As seen in the above example, the pageIndex controls the behaviour of SWR. When the pageIndex changes in state, SWR will request new data from the API. Remember that the SWR makes this new request with a new cached key.

With SWR’s cache, the next page can be preloaded. What is needed to be done is to 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.

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);

  // ... handle loading and error states

  return data.map(item => <div key={item.id}>{item.name}</div>)
}

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  return <div>
    <Page index={pageIndex}/>
    <div className='hidden'><Page index={pageIndex + 1}/></div>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Infinite Loading

useSWRInfinite gives us the ability to trigger a number of requests with one Hook. This is how it looks:

Handling Data Fetching in Next.js with useSWR

Note: If you are using SWR 0.x versions,useSWRInfinite needs to be imported from swr: import { useSWRInifinte } from ‘swr’

useSWRinfinite is similar to useSWR in it implementation. It also accepts a function that returns the request key, a fetcher function, and options. It returns all the values useSWR returns and includes 2 extra values; the page size, the page size setter function like we have in React. In infinite loading, one page is one request, and our goal is to fetch multiple pages and render them.

import useSWRInfinite from 'swr/infinite'

const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // reached the end
  return `/users?page=${pageIndex}&limit=10`                    // SWR key
}

export default function Home() {
  const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
    getKey, fetcher?, options?
  )

  if (!data) return 'loading'

  return (
    <div>
      <p>{totalUsers} users listed</p>
      {data.map((users, index) => {
        // `data` is an array of each page's API response.
        return users.map(user => <div key={user.id}>{user.name}</div>)
      })}
      <button onClick={() => setSize(size + 1)}>Load More</button>
    </div>
  )
}

The default behaviour of useSWRInfinite Hook is to fetch data for each page in a sequence, which implies 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, fetching data sequentially for a large number of pages may not be optimal, particularly if the pages are not interdependent., then you can turn it off by setting the parallel option of useSWRInfinite to true:

The previousPageData argument of the getKey function becomes null when you enable the parallel option.

const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}

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

SSG/ISR/SSR Support

Static Site Generation (SSG) has greatly improved the SEO performance and load time. Although SSG gives us the ability to pre-render pages during build time, client-side data fetching is still essential for real-time views, comments, authentication, and data revalidation.

We are going to be looking at the pre-rendering in Next,js, which can be done with SSG, ISR, or SSR, and fetching client-side data with SWR.

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

Handling Data Fetching in Next.js with useSWR

In the example above, refreshInterval is used to tell useSWR how often it should be revalidated.

Incremental Static Regeneration (ISR), a powerful extension from SSG used to specify how often the page should be regenerated at runtime can replace the refreshInterval option we have inside of useSWR. Alternative to the getStaticProps part of the example above, we can have the below. Remember to remove the refreshInterval from the useSWR Hook.

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

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

Server Side Rendering (SSR) generates a page on the server at runtime. It simply means the contents of the page will always be up to date with the latest data, and it is generated at the time of request.

Say pre-fetching of data is not required in an application, and it is okay for data to be available during request time; the above code can be in this form.

export default function Index({ 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: {
      initialPost,
    },
  };
}

The initialPost will be the post fetched on the server side with the getServerSideProps . On page request, the up-to-date data is rendered. To see this in effect, turn off all the revalidation from SWR to see the dynamic rendering at work.

Mutation and Revalidation

SWR makes available the [mutate](https://swr.vercel.app/docs/mutation#mutate) and [useSWRMutation](https://swr.vercel.app/docs/mutation#useswrmutation) APIs to update the data of a cached key while revalidating whenever/however triggered re-fetches a cached key.

Mutate API can be used in two ways to mutate data.

  1. The global mutate API can mutate any key and
  2. The bound mutate API can mutate the data of the corresponding SWR Hook.

Mutation

Global Mutate

import { useSWRConfig } from "swr"

function App() {
  const { mutate } = useSWRConfig()
  mutate(key, data, options)
}

There is an option to import mutate API globally, but using it with the key parameter without mounting the SWR hook will not update the cache or trigger revalidation. The solution to this will be to mount the SWR hook with the same key first.

import { mutate } from "swr"

function App() {
  mutate(key, data, options)
}
import { useSWRConfig, mutate  } from "swr";

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

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

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

Bound Mutate

This is a short path to mutating the current key with data. It is functionally equivalent to the global mutate API but does not require the key parameter.

import useSWR from 'swr'

function Profile () {
  const { data, mutate } = useSWR('/api/user', fetcher)

  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button 
        onClick={async () => {
          const newName = data.name.toUpperCase()

          // send a request to the API to update the data
          await requestUpdateUsername(newName)

          // update the local data immediately and revalidate (refetch)
          // NOTE: key is not required when using useSWR's mutate as it's pre-bound
          mutate({ ...data, name: newName })
        }}>
        Update user name...
      </button>
    </div>
  )
}

Revalidation

When using mutate API with key mutate(key) or with the bound mutate APPI mutate() without any data, it will trigger a revalidation (mark the data as expired and trigger a re-fetch) for all useSWR hooks that uses that key for global mutate and trigger revalidation for its corresponding useSWR Hook for bound mutate.

import useSWR, { useSWRConfig } from 'swr'

function App () {
  const { mutate } = useSWRConfig()

  return (
    <div>
      <Profile />
      <button onClick={() => {

        // set the cookie as expired
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'

        // tell all SWRs with this key to revalidate
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}

Conditional Fetching With useSWR

Key parameter can also be a function only if it is returning a string, array, object or a falsy value. If the function throws or returns a false value, SWR will not start the request.

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

// conditionally fetch
const { data } = useSWR(condition_to_fetch ? '/api/data' : null, fetcher)

// ...or return a falsy value
const { data } = useSWR(() => condition_to_fetch ? '/api/data' : null, fetcher)

// ...or throw an error when user.id is not defined
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)

Error Handling Strategies With useSWR

When the case of a fetcher function returns an error, as a developer, you either decide to display the error or try a re-fetch. If an error is thrown inside fetcher, it will be returned as an error by the hook.

Sometimes, we want the API to return an error object along with the status code. These are useful information for the client. The fetcher function can be customized in a way that more information is being returned.

const fetcher = async url => {
  const res = await fetch(url)

  // If the status code is not in the range 200-299,
  // we still try to parse and throw it.
  if (!res.ok) {
    const error = new Error('An error occurred while fetching the data.')
    // Attach extra info to the error object.
    error.info = await res.json()
    error.status = res.status
    error.message = "You are not authorized to access this resource."
    throw error
  }
  return res.json()
}

// ...
const { data, error } = useSWR('/api/user', fetcher)

Retrying on Error

SWR uses the [exponential backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) to retry the request in error. This algorithm allows the application to recover from error quickly and not waste resources retrying too often. The onErrorRetry callback function from SWR allows for controlling retry errors based on various conditions. Retry on error can also be disabled by setting shouldRetryOnError: false

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

    // Never retry for a specific key.
    if (key === '/api/specific_key') return

    // Only retry up to 10 times.
    if (retryCount >= 10) return

    // Retry after 5 seconds.
    setTimeout(() => revalidate({ retryCount }), 5000)
  }
})

useSWR With GraphQL

The GraphQL API method of fetching data is similar to fetching from a REST API. The difference is you will need a third-party library to make a request to the GraphQL API. The third-party library for this is [graphql-request](https://github.com/prisma-labs/graphql-request) package.

import { request } from 'graphql-request'

const graphQLFetcher = query => request('/api/graphql', query)

function Movies () {
  const { data, error: isError, loading: isLoading } = useSWR(
    `{
      Movies(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    graphQLFetcher
  )

  if (isError) return <div>Error loading movies</div>;
  if (isLoading) return <div>Loading movies...</div>;
  return (
    <ul>
      {data?.movies.map((movie) => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  );
}

export default Movies;

Mutating a GraphQL API is the same as we have discussed above. You can either use global mutate, bound mutate, or the useSWRMutation Hook.

ADDITION

useSWRSubscription: This Hook is a useful feature of useSWR, it is used for subscribing to real is used to subscribe to real-time data. Let’s have a use case where we have a WebSocket server that sends in real-time news every two seconds.

// 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}`);
  }, 2000);

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

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

Using useSWRSubscription, we can subscribe and listen for events on the client side:

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;

Type Safety With useSWR

SWR supports TypeScirpt out of the box. SWR will also infer the argument type of fetcher from key.

// `key` is inferred to be `string`
const { data } = useSWR('/api/user', key => {})
const { data } = useSWR(() => '/api/user', key => {})

// `key` will be inferred as { a: string; b: { c: string; d: number } }
const { data } = useSWR({ a: '1', b: { c: '3', d: 2 } }, key => {})
const { data } = useSWR(() => ({ a: '1', b: { c: '3', d: 2 } }), key => {})

// `arg0` will be inferred as string.  `arg1` will be inferred as number
const { data } = useSWR(['user', 8], ([arg0, arg1]) => {})
const { data } = useSWR(() => ['user', 8], ([arg0, arg1]) => {})

Types for key and fetcher’s argument can also be explicitly specified:

import useSWR, { Fetcher } from 'swr'

const uid = '<user_id>'
const fetcher: Fetcher<User, string> = (id) => getUserById(id)

const { data } = useSWR(uid, fetcher) // `data` will be `User | undefined`.

The error type thrown inside the fetcher function by default is any and it can be explicitly specified

const { data, error } = useSWR<User, Error>(uid, fetcher);

Performance Optimization With useSWR

Performance is a top priority for SWR because SWR provides critical functionality in all kinds of web applications.

Complex applications could have multiple  useSWR calls in a single page render.

SWR ensures your application does not have unnecessary requests, unnecessary re-renders, or unnecessary code imported without any code changes from you.

  • Deduplication: It is very common to reuse SWR hooks many times in an instance. If useSWR hook is used to construct a component and this component is rendered multiple times on a page or inside the same component. Since the components have the same SWR key and are rendered almost at the same time, only one network request will be made.
function useUser () {
  return useSWR('/api/user', fetcher)
}

function Profile() {
  const { data, error } = useUser()

  if (error) return <Error />
  if (!data) return <Spinner />

  return <img src={data.Profile_url} />
}

function App () {
  return <>
    <Profile />
    <Profile/>
    <Profile/>
    <Profile/>
    <Profile/>
    // ...
  </>
}

The data hook in the code above can be reused everywhere in your application without worrying about performance or duplicated requests.

  • Deep Comparison: SWR deep compares data changes by default. If the value of data remains the same, a re-render will not be triggered.
  • Tree Shaking: The SWR package does not bundle unused APIs into your application if you only import useSWR API. It is tree-shakeable and side-effect-free.
  • Dependency Collection: 4 stateful values are what is returned from useSWR. The returned values are data, error, isLoading, and isValidating.
function App () {
  const { data, error, isLoading, isValidating } = useSWR('/api', fetcher)
  console.log(data, error, isLoading, isValidating)
  return null
}

SWR only updates the states that are consumed by the component. So, if the value(s) is/are not used, there is no need to initialize them.

To every value initialized, it adds to the total number of renderings. If the full data-fetching lifecycle is consoled/printed, the first request failed, and then the retry was successful, it will be this:

// console.log(data, error, isLoading, isValidating)
undefined undefined true true  // => start fetching
undefined Error false false    // => end fetching, got an error
undefined Error true true      // => start retrying
Data undefined false false     // => end retrying, get the data

Say we only use data;

function App () {
  const { data } = useSWR('/api', fetcher)
  console.log(data)
  return null
}

There will only be 2 re-renders.

// ...
// console.log(data)
undefined // => hydration / initial render
Data      // => end retrying, get the data
  • Declarative fetching: useSWR only fetch the data that the current component requires. This prevents unnecessary network calls.
  • Prefetching: The mutate API from useSWR is a good tool for data prefetching, which allows you to update the cached data without making a new network request and keep data up to date in the cache.

Conclusion

With the help of useSWR, fetching, caching, mutating, revalidating and prefetching data are pretty easy and efficient regardless of data size. Each feature of useSWR that was discussed in this article is important and helpful in defensive programming when dealing with client-side data fetching.

Happy Hacking!