Efficient Data Fetching: Demystifying React Query and SWR for Fullstack Developers

Fetching and managing data is a critical part of building web applications, whether you're creating a simple blog or a complex e-commerce platform. However, if done inefficiently, it can lead to poor user experience, performance issues, and maintenance headaches.

Fortunately, there are some modern and powerful libraries that can help you with data fetching, caching, and synchronization, such as React Query and SWR. In this article, we'll explore what these libraries are, how they differ, and how to use them in your fullstack React applications.


What is React Query?

React Query is a library that simplifies and streamlines data fetching in your React components. It provides a declarative and hook-based API that allows you to fetch data from one or multiple sources, such as REST APIs, GraphQL servers, or local storage. React Query also offers features like caching, pagination, polling, and optimistic updates, which can improve the performance and user experience of your app.

Basic Usage

Let's start by looking at a simple example of fetching data with React Query. Suppose we have a component that displays a list of posts fetched from a REST API:

{`
    import React from 'react';
    import { useQuery } from 'react-query';
    
    const fetchPosts = async () => {
      const response = await fetch('/api/posts');
      const data = response.json();
      return data;
    };
    
    const PostList = () => {
      const { data, isLoading, error } = useQuery('posts', fetchPosts);
    
      if (isLoading) return 

Loading...

; if (error) return

{error.message}

; return (
    {data.map(post => (
  • {post.title}
  • ))}
); }; `}

In this example, we define a fetchPosts function that uses the Fetch API to retrieve the list of posts from our API. Then, we use the useQuery hook from React Query to fetch the data and cache it under the key 'posts'. We also handle the loading and error states with conditional rendering.

Caching

One of the most useful features of React Query is caching. Once you fetch data with a certain key, React Query will automatically store it in a cache, and subsequent requests with the same key will be served from the cache instead of the network, unless the data is stale. This can significantly reduce the number of network requests and speed up your app.

You can configure the caching behavior of React Query in various ways, such as setting a TTL (time-to-live) for the data, keeping only a certain number of items in the cache, or using a custom cache provider. You can also manually invalidate or refetch the data, for example when the user takes a certain action or when a related data source changes.

Pagination

Another common use case of data fetching in web applications is pagination, where you fetch only a portion of the data at a time, and load more data when the user reaches the end of the current page. React Query provides built-in support for pagination, either by using an offset-based or a cursor-based approach.

To use offset-based pagination with React Query, you can pass a page number and a page size as variables to the query key, and use them to compute the range of data to fetch:

{`
    const fetchPosts = async (key, page = 1, pageSize = 10) => {
      const offset = (page - 1) * pageSize;
      const response = await fetch(`/api/posts?offset=${offset}&limit=${pageSize}`);
      const data = response.json();
      return data;
    };

    const PostList = () => {
      const { data, isLoading, isError, error } = useQuery(['posts', page], fetchPosts);

      // ...
    };
  `}

In this example, we use the page and pageSize variables as part of the query key, so that React Query knows when to refetch the data with a different page number. We also pass the same variables to the fetchPosts function to compute the offset and page size for our API call.

Polling

If you need to continuously fetch and update data in your app, for example to display real-time information or notifications, you can use polling with React Query. Polling is a technique where you periodically fetch data from a server at a specific interval, and refresh the UI with the new data.

React Query provides a useQuery option called refetchInterval, which allows you to set the interval in milliseconds between successive fetches. You can also use other options like refetchIntervalInBackground, refetchOnMount, and refetchOnWindowFocus to fine-tune the polling behavior. If you need more control over the polling process, you can use the pollingInterval option and manually call the refetch function to trigger a new fetch:

{`
    const PostList = () => {
      const { data, isLoading, isError, error, refetch } = useQuery('posts', fetchPosts, {
        refetchInterval: 3000,
      });

      // ...

      return (
        <>
          
    {data.map(post => (
  • {post.title}
  • ))}
); }; `}

In this example, we set the refetchInterval option to 3000 milliseconds (3 seconds), and provide a refetch function that can be called manually to force a new fetch. We also render a "Refresh" button that triggers the refetch function when clicked. This way, the user can choose to refresh the data at any time, or let the automatic polling do its job.


What is SWR?

SWR (short for "stale-while-revalidate") is another library that aims to simplify data fetching and caching in React applications. It uses a similar hook-based API to React Query, but with a different approach to caching and synchronization. SWR focuses on optimizing the balance between responsiveness and consistency of your app, by leveraging a local cache, a remote cache, and a globally consistent data layer.

Basic Usage

Let's see a simple example of using SWR to fetch data from a REST API:

{`
    import React from 'react';
    import useSWR from 'swr';
    
    const fetcher = async (url) => {
      const response = await fetch(url);
      const data = response.json();
      return data;
    };
    
    const PostList = () => {
      const { data, error } = useSWR('/api/posts', fetcher);
    
      if (error) return 

{error.message}

; return (
    {data.map(post => (
  • {post.title}
  • ))}
); }; `}

In this example, we define a fetcher function that takes a URL and returns the response as JSON. We use the useSWR hook from SWR to fetch the data from the URL, and let SWR handle the caching and synchronization for us. We also handle the error state with conditional rendering.

Caching

SWR has a built-in caching mechanism that operates on a per-resource basis. When you fetch data with SWR, it first checks if the data is available in its local cache (which can be in-memory or localStorage), and returns it if it's not stale. Then, it sends a request to the API to get the latest data, and updates the cache with the new data. If the API responds with a 304 "Not Modified" status, it means the data is still fresh, and SWR keeps using the previous data.

You can configure the caching behavior of SWR in various ways, such as setting a TTL, enabling or disabling revalidation on focus or reconnection, or providing a custom cache provider. You can also manually mutate the cache with new data, for example when the user updates a resource locally or when a WebSocket sends a new message.

Synchronization

One of the unique features of SWR is its ability to synchronize data across different instances of your app, even if they are running in different tabs or devices. This is achieved by using a globally consistent data layer, which is a distributed cache that implements a "push-based" approach to updating data. Whenever a mutation occurs on one instance of your app, such as creating a new post or updating a comment, the data layer broadcasts the change to all other instances that are currently active, and triggers a revalidation on their caches. This ensures that all users of your app see the same data in real-time, without the need for manual refreshing or polling.

Optimistic Updates

SWR also supports optimistic updates, which is a technique that improves the perceived speed and interactivity of your app by immediately updating the UI with new data before the server confirms the mutation. This can give the user an instant feedback that their action was successful, and avoid the frustration of waiting for the server to respond. Optimistic updates can be implemented by using the mutate function provided by the useSWR hook:

{`
    const PostList = () => {
      const { data, mutate } = useSWR('/api/posts', fetcher);

      const handleCreatePost = async (title, content) => {
        const response = await fetch('/api/posts', {
          method: 'POST',
          body: JSON.stringify({ title, content }),
        });

        if (response.ok) {
          const newPost = await response.json();
          // Update the cache optimistically
          mutate((data) => [newPost, ...data], false);
        }
      };

      // ...

      return (
        <>
          

          
    {data.map(post => (
  • {post.title}
  • ))}
); }; `}

In this example, we define a handleCreatePost function that sends a POST request to the API to create a new post. If the request succeeds, we update the SWR cache optimistically by calling the mutate function with a new data object that includes the newly created post. The second parameter of mutate is set to false to avoid a revalidation until the server confirms the mutation. We also render a PostForm component that allows the user to create new posts.


Which one to use?

Now that we've seen the main features and usage patterns of both React Query and SWR, you might wonder which one to choose for your next project. The answer depends on your specific needs and preferences, but here are some general guidelines:

  • Use React Query if you value simplicity, flexibility, and performance. React Query is easy to learn and use, supports a wide range of data sources and features, and has a lean and efficient implementation. It's also well-suited for handling complex queries and mutations, and provides a lot of customization options.
  • Use SWR if you value consistency, synchronization, and developer experience. SWR is designed for optimizing the balance between local and remote caching, provides a seamless integration with global data layers like Apollo or Firebase Realtime Database, and has a powerful toolset for debugging and error handling. It's also very intuitive to use, especially if you're familiar with React state management.

Of course, you don't have to choose only one library and use it for all data fetching needs. You can mix and match React Query and SWR, or even use other libraries like Axios or Fetch directly, depending on the context and requirements of your app. The important thing is to keep your data fetching code clean, modular, and reusable, so that you can focus on the business logic and user experience of your app.


Conclusion

Data fetching is a crucial aspect of building web applications, and should be done efficiently and effectively. React Query and SWR are two valuable libraries that can help you with that, by providing simple, powerful, and flexible abstractions for fetching, caching, and syncing data in your React components. Whether you prefer the simplicity and customization of React Query, or the consistency and synchronization of SWR, you now have the tools to choose the best option for your specific use case. Happy coding!