Featured resource
pluralsight tech forecast
2025 Tech Forecast

Which technologies will dominate in 2025? And what skills do you need to keep up?

Check it out
Hamburger Icon
  • Labs icon Lab
  • Core Tech
Labs

Guided: State Management with SWR

This code lab will teach you how to implement efficient state management in React applications using SWR. You'll learn to fetch and update data, handle errors, and implement advanced features like infinite scrolling. By the end of this lab, you'll have practical experience in building web applications with optimized data handling.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 24m
Published
Clock icon Sep 23, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: State Management with SWR.

    SWR is a React Hooks library created by the team at Vercel to handle data fetching, caching, and revalidation in React applications. SWR stands for Stale-While-Revalidate, an HTTP cache invalidation strategy defined by HTTP RFC 5861.

    The core concept behind SWR is to first return cached (stale) data while revalidating it in the background, ensuring that the UI always displays up-to-date information. SWR abstracts away many of the complexities involved in state management, data fetching, and revalidation, allowing you to focus on building features instead of worrying about state consistency.

    While React has several options for managing state, SWR excels in scenarios where the state is tied closely to remote data. It's especially useful for applications that need to reflect data from a backend API, offering a high-level solution that combines fetching, caching, and revalidating into a simple, intuitive API.

    In this lab, you'll dive into the fundamentals of SWR by implementing state management in a simple notes manager application. You'll learn how to:

    • Fetch and display data using the useSWR hook.
    • Handle errors and loading states gracefully.
    • Implement real-time data updates using SWR's revalidation features.
    • Manage paginated data with infinite scrolling using the useSWRInfinite hook.

    The application allows users to view a list of existing notes, which can be toggled between a standard list and an infinite-scrolling list. To add a new note, users will fill out a form by entering a title and content. Upon form submission, the note will be added to the list displayed on the page. Users can delete notes by clicking a delete button next to each note.

    By the end of this lab, you'll have a solid understanding of how to use SWR for efficient state management in your React applications. ### Familiarizing with the Program Structure

    The application includes the following files:

    • App.js: The main React component that structures the application.
    • NoteForm.js: A React component for adding new notes.
    • NotesList.js: A React component for displaying all the available notes in a standard list.
    • NotesInfiniteList.js: A React component for displaying notes with infinite scrolling.
    • api.js: A file containing simulated API functions for note operations.

    The standard and the infinite-scrolling lists are different components because there are significant differences in their implementations.

    You'll focus on the NotesList.js, NoteForm.js, and NotesInfiniteList.js files, implementing the missing functionality.

    Start by examining the provided code to understand the program structure. Once you're ready, begin coding. If you run into any issues, a solution directory is available for reference. It contains subdirectories named step1, step2, and so on, each corresponding to a step in the lab. Within each subdirectory, solution files follow the naming convention [file]-[step number]-[task number].js (e.g., NotesList-1-1.js in step1), where [file] represents the file name, the first number indicates the step, and the second represents the task.

  2. Challenge

    Step 1: Basic Data Fetching with SWR

    Using the useSWR Hook

    At the core of SWR is the useSWR hook, which provides an API for handling data fetching, caching, and revalidation.

    The useSWR hook takes at least two arguments, a key and a fetcher function:

    1. Key: The first argument is a unique key that identifies the data. This key helps SWR to cache and manage the data effectively. It's commonly a string representing the endpoint or resource you are fetching.

    2. Fetcher Function: The second argument is a function that performs the actual data fetching. This function can use fetch, axios, or any other method to retrieve data from an API.

    The hook returns an object containing the following properties:

    • data: The fetched data.
    • error: Any error encountered during the fetch.
    • isLoading: A boolean indicating if there's an ongoing request and no loaded data. Fallback data and previous data are not considered loaded data.
    • isValidating: A boolean indicating if there's a request or revalidation loading.
    • mutate(data?, options?): A function to mutate the cached data.

    Here's an example to illustrate the usage of the useSWR hook:

    import useSWR from 'swr';
    
    // Define a fetcher function
    const fetcher = async (url) => {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      return response.json();
    };
    
    function ExampleComponent() {
      // Use the useSWR hook to fetch data
      const { data, error, isValidating } = useSWR('/api/data', fetcher);
    
      // Handle error state
      if (error) return <div>Error: {error.message}
    
      // Handle loading state
      if (isValidating) return <div>Loading...</div>;</div>;
    
      // Render the fetched data
      return (
        <div>
          <h1>Fetched Data</h1>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      );
    }
    

    In this example:

    • The fetcher function uses fetch to retrieve data from the /api/data endpoint and returns the JSON response.
    • The useSWR hook is called with the key /api/data and the fetcher function.
    • The useSWR hook returns an object with a property named data. By default, data would be accessed directly by its name.
    • The component handles the loading state by checking isValidating and displaying a loading message.
    • It handles the error state by checking error and displaying an error message.
    • Finally, it renders the fetched data when available.

    When you use useSWR, the hook automatically handles several critical aspects for you:

    • Caching: SWR caches the fetched data based on the key, ensuring that subsequent requests for the same key return the cached data instantly.
    • Revalidation: SWR revalidates the data in the background to keep it up-to-date, fetching new data whenever the component re-renders or the key changes.
    • Error Handling: SWR provides a built-in mechanism for handling errors, allowing you to define custom error handling logic.

    Now that you understand how useSWR works, you're ready to fetch the notes from the simulated API in the application. The useSWR hook will fetch data and store the fetched notes in the notes variable to display them.

    Additionally, you can show a message while loading the data. You'll do that in the next task by using the isValidating property (to know when a revalidation is happening).

  3. Challenge

    Step 2: Handling Errors

    Error Handling in SWR

    Error handling is an important aspect of data fetching, and SWR provides built-in mechanisms to handle errors gracefully. When using useSWR, you can add two specific callback functions to handle errors:

    1. onError(err, key, config): This callback is triggered when a request returns an error. It provides the error object, the key used for the request, and the SWR configuration.

    2. onErrorRetry(err, key, config, revalidate, revalidateOps): This callback handles error retries. It provides additional arguments, including a function to trigger revalidation and an object containing retry options (e.g., the retry count).

    Also, useSWR returns an error object. This object contains the error thrown by the fetcher function, or it is undefined if no error occurred.

    Here's an example that illustrates error handling in SWR:

    import useSWR from 'swr';
    
    const fetcher = async (url) => {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      return response.json();
    };
    
    function ExampleComponent() {
      const { data, error, isValidating } = useSWR('/api/data', fetcher, {
        onErrorRetry: (error, key, config, revalidate, revalidateOps) => {
          // Retry up to 2 times
          if (revalidateOps.retryCount >= 2) return;
          
          // Retry after 5 seconds
          setTimeout(() => revalidate(revalidateOps), 5000);
        }
      });
    
      if (error) return <div>Error: {error.message}</div>;
      if (isValidating) return <div>Loading...</div>;
    
      return (
        <div>
          <h1>Fetched Data</h1>
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      );
    }
    

    In this example:

    • The onErrorRetry callback retries the request up to 2 times, with a 5-second delay between retries.
    • The error object is checked to display an error message if an error occurs during data fetching.

    Now that you understand how to handle errors with useSWR, you're ready to apply this knowledge in the next tasks.

  4. Challenge

    Step 3: Mutations and Revalidation

    Mutations and Revalidation in SWR

    Mutations and revalidation allow you to update and synchronize the local cache with the remote data source efficiently.

    A mutation in SWR refers to updating the local data cache. This is particularly useful when you need to update the UI optimistically before the actual data operation (like adding, updating, or deleting data) is confirmed by the backend. The useSWRConfig hook provides a mutate function that you can use to update the cache for a specific key.

    For example, if you want to update the cache for a list of items after adding a new item, you can use the mutate function to trigger a revalidation and fetch the updated list from the server.

    Revalidation ensures that the data displayed in your application is always up-to-date by marking the data as expired and triggering a refetch. In the previous step, you used the revalidate function in the onErrorRetry callback to retry fetching data after an error. Similarly, you can use the mutate function to revalidate data after performing a mutation. This helps keep the UI in sync with the latest data.

    Here's an example to illustrate how to use mutations and revalidation in SWR:

    import useSWR, { useSWRConfig } from 'swr';
    
    const fetcher = async (url) => {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      return response.json();
    };
    
    function ExampleComponent() {
      const { data, mutate } = useSWR('/api/data', fetcher);
      const { mutate: globalMutate } = useSWRConfig();
    
      const handleAddItem = async () => {
        // Optimistically update the cache
        await globalMutate('/api/data', (data) => [...data, newItem]);
    
        // Make the actual API call
        await fetch('/api/data', {
          method: 'POST',
          body: JSON.stringify(newItem),
        });
    
        // Revalidate the cache to ensure data consistency
        await globalMutate('/api/data');
      };
    
      return (
        <div>
          <h1>Fetched Data</h1>
          <pre>{JSON.stringify(data, null, 2)}</pre>
          <button onClick={handleAddItem}>Add Item</button>
        </div>
      );
    }
    

    In this example:

    • The useSWRConfig hook provides a global mutate function for updating the cache.
    • The useSWR hood also returns a mutate function to mutate the cached data. It is functionally equivalent to the global mutate function from useSWRConfig but does not require the key parameter.
    • The mutate function is used to optimistically update the cache (updating the local data manually while waiting for the remote mutation to finish) and trigger revalidation after performing an API call.
    • The globalMutate function ensures that the cache is consistent with the latest data from the server.

    These are the parameters of the mutate function:

    • key: Same as useSWR's key, but if it's a function, it behaves as a filter function.
    • data: (Optional) Data to update the client cache, or an async function for the remote mutation.
    • options: (Optional) Accepts the following options:
      • optimisticData: Data to immediately update the client cache, or a function that receives current data and returns the new client cache data, usually used in optimistic UI.
      • revalidate = true: Should the cache revalidate once the asynchronous update resolves. If set to a function, the function receives data and key.
      • populateCache = true: Should the result of the remote mutation be written to the cache, or a function that receives new result and current result as arguments and returns the mutation result.
      • rollbackOnError = true: Should the cache rollback if the remote mutation errors, or a function that receives the error thrown from fetcher as arguments and returns a boolean whether should rollback or not.
      • throwOnError = true: Should the mutate call throw the error when fails.

    Now that you understand the concepts of mutation and revalidation, you're ready to apply this knowledge in the next tasks. After completing the task, the SWR cache will be updated after adding a new note.

    However, you also need to revalidate the cache after deleting a note. After completing the task, the SWR cache will be updated after deleting a note.

  5. Challenge

    Step 4: Infinite Loading

    Infinite Loading and Pagination in SWR

    In SWR, you can implement both normal pagination and infinite loading as mechanisms to handle large sets of data efficiently. Infinite loading is particularly useful for applications where you want to load more data as the user scrolls down the page, providing a seamless user experience.

    To implement infinite pagination, SWR provides the useSWRInfinite hook. This hook allows you to fetch data in pages and manage state for paginated data. The useSWRInfinite hook can take three arguments:

    1. getKey: A function that accepts the index and the previous page data, returns the key of a page.
    2. fetcher: A function that performs the actual data fetching for each page (same as useSWR's fetcher function).
    3. options: Accepts all the options that useSWR supports, with 4 extra options: initialSize = 1: Number of pages should be loaded initially (it's is not allowed to change after set). revalidateAll = false: Always try to revalidate all pages. revalidateFirstPage = true: Always try to revalidate the first page. persistSize = false: Don't reset the page size to 1 (or initialSize if set) when the first page's key changes. parallel = false: Fetches multiple pages in parallel.

    The useSWRInfinite hook returns an object containing these values:

    • data: An array of fetch response values of each page.
    • error: Same as useSWR's error.
    • isLoading: Same as useSWR's isLoading.
    • isValidating: Same as useSWR's isValidating.
    • mutate: Same as useSWR's bound mutate function but manipulates the data array.
    • size: The number of pages that will be fetched and returned.
    • setSize: Set the number of pages that need to be fetched.

    Here's an example to illustrate the usage of useSWRInfinite:

    import useSWRInfinite from 'swr/infinite';
    
    const fetcher = async (url) => {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error('Failed to fetch data');
      }
      return response.json();
    };
    
    function ExampleComponent() {
      const {
        data, 
        error,
        size,
        setSize, 
        mutate,
        isValidating,
        isLoading,
      } = useSWRInfinite(
        (index) => `/api/data?page=${index + 1}`, // Key function
        fetcher                                  // Fetcher function
      );
    
      if (isLoading) return <div>Loading...</div>;
      if (error) return <div>Error: {error.message}</div>;
    
      return (
        <div>
          {data.map((page, i) => (
            <div key={i}>
              {page.map(item => (
                <div key={item.id}>{item.name}</div>
              ))}
            </div>
          ))}
          <button onClick={() => setSize(size + 1)}>Load More</button>
        </div>
      );
    }
    

    In this example:

    • The key function generates a unique key for each page based on the page number.
    • The fetcher function fetches data for each page.
    • The setSize function is used to load more pages.

    When you need to revalidate the data globally (for instance, after adding a new item), you have to use the unstable_serialize function from swr/infinite. This function serializes the key function used in useSWRInfinite so you can revalidate the cache globally.

    This approach is necessary when the mutation and revalidation are performed in a different component from the one that fetched the data.

    Here's how you can use unstable_serialize:

    import { useSWRConfig } from "swr";
    import { unstable_serialize } from "swr/infinite";
    
    function App() {
      const { mutate } = useSWRConfig();
      mutate(unstable_serialize((index) => `/api/data?page=${index + 1}`));
    }
    

    This example shows how to use unstable_serialize to revalidate the paginated data globally. Note that unstable_serialize is not a stable API and might change in the future. For the latest information on this API, refer to the SWR documentation on pagination.

    Now that you understand how infinite pagination works in SWR, you're ready to apply this knowledge in the next tasks.

  6. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    This lab covers just the fundamentals of SWR. For more information, read the guides on the SWR documentation page.

    To run the application, either click the Run in the bottom-right corner of the screen or in a Terminal tab execute:

    npm start
    

    Then, click the following link to open the application in a new browser tab: {{localhost:3000}}. Alternatively, you can refresh the Web Browser tab to reload and view the application there.

    For the standard list, the method getNotes from the simulated API (api.js) simulates a 30% chance of failure to demonstrate error handling, so sometimes, when getting the notes (by refreshing the page or by SWR revalidating the notes), you'll get an error message, but the application should retry automatically to load the notes correctly. ---

    Extending the Program

    Consider exploring these ideas to further enhance your skills and expand the capabilities of the program:

    1. Normal Pagination in Standard List: Implement normal pagination in the standard notes list. Add controls to navigate between different pages of notes and update the data fetching logic to load a specific page of notes based on the selected page number.

    2. Optimistic Updates: Implement optimistic updates when adding or deleting notes. Update the UI immediately to reflect the changes before the server confirms them. If the server operation fails, revert the changes in the UI.

    3. Note Editing: Implement functionality to edit existing notes. Add an edit button next to each note that allows users to modify the title and content. Update the note in the backend and revalidate the notes list to reflect the changes. ---

    Related Courses on Pluralsight's Library

    If you're interested in further honing your React skills or exploring more topics, Pluralsight offers several excellent courses in the following path:

    These courses cover many aspects of React programming. Check them out to continue your learning journey in React!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.