Simplify Data Fetching in React with the SWR Library: A Comprehensive Guide

Fetching data from APIs is a crucial part of many modern web applications. But it also introduces a lot of potential challenges and edge cases to consider. How do you handle caching and avoid unnecessary requests? What about revalidation and always displaying the latest data? And how do you manage complex loading and error states?

While React itself doesn‘t prescribe a specific approach for data fetching, a library called SWR provides an elegant and efficient solution. SWR stands for "stale-while-revalidate", a powerful strategy for always displaying the most up-to-date data in your UI with minimal effort.

In this guide, we‘ll take a deep dive into the SWR library and learn how to use it to drastically simplify data fetching in your React applications. Whether you‘re new to SWR or looking to fully harness its potential, this post will equip you with the knowledge and patterns you need.

The Challenges of Data Fetching in React

First, let‘s briefly review some of the common challenges you face when fetching data in React. While you can certainly use the native browser fetch API or a library like Axios directly in your components, you quickly run into questions like:

  • How do I avoid making duplicate requests for the same data?
  • How can I cache responses and serve them instantly on subsequent renders?
  • If the data changes on the server, how do I "invalidate" the cache and fetch the latest version?
  • How should I handle race conditions and out-of-order responses?
  • What‘s the best way to manage loading and error states?

Solving all of these problems on your own is certainly possible, but it requires a significant amount of code and careful consideration. This is where SWR comes in and handles the hard parts for us.

What is SWR?

SWR is a React Hooks library for data fetching developed by Vercel. While its name stands for "stale-while-revalidate", I personally like to think of it as "She Who Revalidates" – that‘s how magical it feels sometimes!

At its core, SWR provides a simple and reusable way to declare a remote data dependency and keep it up-to-date throughout the lifecycle of your component. You define your data fetching logic once and then simply use the data in your components. SWR handles caching, revalidation, and more.

Under the hood, it implements the "stale-while-revalidate" caching strategy, which means:

  1. It will always first return the data from cache (if available), even if it‘s "stale" or potentially out of date.
  2. It then sends a fetch request to revalidate the data, and triggers a rerender with the updated data if needed.
  3. This provides an excellent user experience as content is always available instantly, even if it‘s not always the latest version.

Let‘s look at a quick example to see SWR in action:

import useSWR from ‘swr‘;

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

function Profile() {
  const { data, error } = useSWR(‘/api/user‘, fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;

  return <div>Hello {data.name}!</div>;
}

In this example, we import the useSWR hook from the swr package. We then define a fetcher function that takes a URL and returns a promise that resolves to the parsed JSON response.

Inside our component, we call useSWR and pass it the URL we want to fetch data from and our fetcher function. It returns an object with data and error properties, representing the current state of the request.

We then render different contents based on that state:

  • If there‘s an error, we display a failure message
  • If data is falsy (i.e. null on the first render), we display a loading message
  • Otherwise, we display a welcome message with the user‘s name

The key thing to note is that we didn‘t have to manually initiate any loading state, track whether the request finished, store the response in component state, or handle any of the edge cases we mentioned earlier.

All of that complexity is abstracted away by SWR, allowing you to focus on simply declaring what data your component needs and then using that data.

Key Features and Benefits of SWR

Now that we‘ve seen a high-level example, let‘s dive deeper into some of the key features and benefits of SWR.

Automatic Caching

One of the primary benefits of SWR is its automatic caching capabilities. Whenever you make a request with useSWR, it will store the response in a global cache.

If you call useSWR with the same key (usually a URL) in multiple components, it will return the cached data instantly if available. This means you can deduplicate requests across your application and avoid unnecessary network requests.

You can also preload data into the cache ahead of time using the SWRConfig component. This could be useful for scenarios like server-side rendering, where you might already have fetched some data on the server that you want to reuse on the client.

Revalidation and Optimistic UI

The "stale-while-revalidate" part of SWR doesn‘t just refer to the initial caching behavior. It also means that SWR will automatically revalidate the data in the background at certain intervals and update your UI optimistically.

For example, even if a request is made to an endpoint that has previously returned data, SWR will still show the "stale" cached data first. It then triggers a new request to fetch the latest data from your API. If the response differs from what‘s in the cache, it updates the cache and your UI.

This optimistic approach provides a great user experience as content is always visible and up-to-date. It also helps mask latency of the network and keeps your application feeling responsive.

SWR even automatically revalidates data when you switch tabs or reconnect to the network after being offline. This ensures your users are always seeing fresh data without you as the developer having to manually handle those cases.

Pagination and Infinite Loading

Another powerful feature of SWR is its built-in support for pagination and infinite loading. With APIs that return paginated lists of data, SWR provides a simple way to fetch and merge multiple pages together automatically.

Consider an example where you have an API route that returns a list of products, 10 at a time. You can fetch and display this data incrementally with SWR like so:

import useSWRInfinite from ‘swr/infinite‘

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

function Products () {
  const { data, size, setSize } = useSWRInfinite(getKey)
  if (!data) return ‘Loading...‘

  // Flatten and display the array of products from each page request
  const products = data.flat()
  return <div>
    {products.map(product => <Product key={product.id} name={product.name} />)}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

In this example, we import the useSWRInfinite hook, which is a special version of useSWR designed for paginated data.

We provide it a getKey function that accepts the current page index and the data from the previous page. It returns the URL for the next page, or null if there are no more pages.

The useSWRInfinite hook returns an array of the fetched pages as data, the current number of pages as size, and a setSize function to load more pages.

Inside our component, we flatten the array of data into a single list of products and render them. We also display a "Load More" button that increments the size to fetch the next page of data.

Whenever setSize is called, SWR will automatically fetch the next page of data, merge it into the existing cache, and update the data in our component. We can keep calling this function to progressively load and display more products.

This pagination and infinite loading support dramatically simplifies what would otherwise be an error-prone and tedious process to implement manually.

Advanced SWR Patterns

So far we‘ve covered the basics of using SWR to fetch and cache remote data in your React components. Next, let‘s explore some more advanced patterns and use cases.

Custom Hooks

Just like React‘s built-in Hooks like useState and useEffect, SWR is designed to be composable. This means you can easily create your own custom hooks that wrap useSWR for specific data fetching use cases.

For example, let‘s say your application frequently needs to fetch user data from an /api/user endpoint. Rather than calling useSWR directly in multiple components, you could create a reusable useUser hook like this:

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

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

This hook accepts a user ID, calls useSWR with the appropriate URL, and returns an object containing the user data, loading state, and error state.

You can then easily reuse this hook anywhere you need user data:

function UserProfile({ userId }) {
  const { user, isLoading, isError } = useUser(userId)

  if (isLoading) return <div>Loading user...</div>
  if (isError) return <div>Failed to load user</div>

  return <div>Name: {user.name}</div>
}

By encapsulating the data fetching logic in a custom hook, you keep your components clean and focused on rendering UI. The hook also provides a clean and consistent interface for loading and error states.

Mutating Data

In addition to reading data with SWR, you may also need to mutate that data when users perform actions like submitting a form.

SWR provides a mutate function that lets you programmatically update the cached data for a given key. This is useful for optimistically updating the UI after a mutation and then revalidating the data with the server.

Here‘s an example of using mutate to update a user‘s profile:

function UserProfileForm({ userId }) {
  const { user, mutate } = useUser(userId)

  async function handleSubmit(event) {
    event.preventDefault()
    const formData = new FormData(event.target)

    // Optimistically update the cache
    mutate({ ...user, name: formData.get(‘name‘) }, false)

    // Send the update to the server
    await fetch(`/api/user/${userId}`, {
      method: ‘PATCH‘,
      body: JSON.stringify({ name: formData.get(‘name‘) })
    })

    // Revalidate the data from the server
    mutate()
  }

  return <form onSubmit={handleSubmit}>
    <input name="name" defaultValue={user.name} />
    <button type="submit">Update Name</button>
  </form>
}

In this example, when the form is submitted, we first optimistically update the cached user data with the new name. The second argument to mutate is false, telling SWR not to revalidate the data yet.

We then send the update to the server with a PATCH request. When that completes, we call mutate again without any arguments, which triggers a revalidation and updates the cache with the latest data from the server.

This pattern provides a great user experience. The UI updates instantly when the user submits the form, but we still ensure the data is consistent with the server by revalidating after the mutation.

Error Handling

We‘ve seen basic error handling in previous examples, where we check the error property returned by useSWR and render an error message if it‘s truthy.

But SWR also provides more powerful ways to handle and recover from errors.

First, you can provide an onError option to the useSWR hook to catch and handle errors globally:

const { data, error } = useSWR(‘/api/user‘, fetcher, {
  onError: (error, key, config) => {
    if (error.status !== 403 && error.status !== 404) {
      // Trigger an error alert
      alert(‘Error loading data!‘);
    }
  }
})

In this example, we‘re using the onError option to show an alert if the error is not a 403 or 404. This is useful for handling unexpected errors globally without having to check for them in each component.

You can also provide a fallback option to useSWR that will be returned as the data if there‘s an error:

const { data, error } = useSWR(‘/api/user‘, fetcher, {
  fallback: { name: ‘Placeholder‘ }
})

This is useful for providing a fallback UI that still displays something useful in the event of an error. In this case, we‘re returning a placeholder user object with a default name.

Conclusion

In this comprehensive guide, we‘ve learned how to use the SWR library to dramatically simplify data fetching in React applications.

We‘ve seen how SWR‘s "stale-while-revalidate" caching strategy provides an excellent user experience by always displaying cached data instantly, then revalidating it in the background to ensure freshness.

We‘ve also explored more advanced features and patterns like pagination, optimistic mutation, and global error handling.

By abstracting away the complexities of caching, revalidation, and other common data fetching challenges, SWR lets you focus on the core logic of your application. Its composable, hook-based API also leads to cleaner and more reusable code.

The next time you‘re dealing with remote data in a React application, I highly recommend reaching for SWR. It‘s a powerful and elegant solution that will save you time and frustration.

Thanks for reading! I hope this guide has been helpful in your journey to master data fetching with SWR and React. As always, feel free to reach out with any questions or feedback.

Happy coding!

Similar Posts