Shalitha Suranga Programmer | Author of Neutralino.js | Technical Writer

Caching clash: SWR vs. TanStack Query for React

15 min read 4308

Caching clash: SWR vs. TanStack Query for React

Most React applications communicate with remote data sources to persist and retrieve data records. Web application development teams nowadays tend to use REST and GraphQL-like communication patterns to implement their remote data source interfaces. Then, frontend development teams have to make network requests with various libraries through their React apps to sync data between the client side and the server side.

For communicating with RESTful services, the simplest way is to use the inbuilt Fetch API or a library like Axios in the component to mount state-like events. Then, you have to write additional logic to implement loading state UI enhancements. Finally, to make your app even more user-friendly and optimized via data caching, deduplicated API queries, and pre-fetching, you may have to write more code than your client-side business logic!

This is where libraries like SWR and TanStack Query — formerly React Query — can help you sync your data source’s state with your React app’s state via caching, pre-fetching, query deduplication, and various other usability features.

In this article, I will compare the features of SWR and the TanStack Query library with a practical example project. Here’s what we’ll cover:

What is React SWR?

SWR is an open source, lightweight, and TypeScript-ready library that offers several Hooks for fetching data in React with caching. The abbreviation “SWR” stands for State While Re-validate, a generic caching principle from HTTP RFC 5861.

React SWR was first released in 2019 via its v0.1.2 public release.

Highlighted features

This library offers the following highlighted features:

Feature Description
Lightweight size and high performance According to BundlePhobia, the SWR library weighs ~4.2 kilobytes when gzipped. The SWR development team focuses on performance and being lightweight with the tree-shaking bundling strategy
Minimal, configurable, and re-usable API SWR also focuses on offering a minimal, developer-friendly API for React developers that provides performance-friendly features. You can implement most of the things you need with a single Hook, useSWR.

Even though the API is minimal, it lets you tweak the caching system and behavior with a global configuration and many Hook options.

Inbuilt features for developers and users SWR supports paginated requests and provides the useSWRInfinite Hook to implement infinite loading. It also works with the React Suspense API, SSG, and SSR, and offers pre-fetching, re-validation on focus, and network status re-fetching, like usability enhancements for app users.

Using React SWR

Now that we have an overview of SWR’s features for optimized data fetching in React, let’s create a sample app with SWR and evaluate it to find comparison points with TanStack Query.

We can mock our API backend with delayed promises on the client side to try SWR, but that approach doesn’t give a real data fetching experience. Let’s instead create a simple RESTful API with Node.js. We can create a RESTful API server in seconds with the json-server package.

First, install the json-server package globally:

npm install -g json-server
# --- or ---
yarn global add json-server

Next, add the following content to a new file named db.json:

{
  "products": [
    {
      "id": 1,
      "name": "ProX Watch",
      "price": 20
    },
    {
      "id": 2,
      "name": "Magic Pencil",
      "price": 2
    },
    {
      "id": 3,
      "name": "RevPro Wallet",
      "price": 15
    },
    {
      "id": 4,
      "name": "Rice Cooker",
      "price": 25
    },
    {
      "id": 5,
      "name": "CookToday Oven",
      "price": 10
    }
  ]
}

Next, run the following command to start a RESTful CRUD server based on the db.json file:

json-server --watch --port=5000 --delay=1000 db.json

Now, we can access our CRUD API via http://localhost:5000/products. You can test it with Postman if you want. In our example, we added a 1000ms delay to simulate network latency.

Let’s create a new React app and fetch data via SWR. If you are already an SWR user or you’ve experimented with SWR before, you can check the complete project in this GitHub repository and continue to the TanStack Query section below.

Create a new React app, as usual:

npx create-react-app react-swr-example
cd react-swr-example

Installing the package

Next, install the swr package with the following command:

npm install swr
# --- or ---
yarn add swr

We’ll use Axios in this tutorial, so install it, too, with the following command. You can use any HTTP request library or the inbuilt fetch, since SWR expects just promises.

npm install axios
# --- or --- 
yarn add axios

Implementing data fetchers and custom Hooks

We will evaluate SWR by creating a simple product management app that lists some products and lets you add new ones. First, we need to store our local mock API’s base URL in the .env file. Create a new file named .env and add the following content:

REACT_APP_API_BASE_URL = "http://localhost:5000"

Next, use the base URL in the Axios global configuration by adding the following content to the index.js file:

import React from 'react';
import ReactDOM from 'react-dom/client';
import axios from 'axios';

import './index.css';
import App from './App';

axios.defaults.baseURL = process.env.REACT_APP_API_BASE_URL;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

We’ll keep all app components in our App.js file to maintain the tutorial’s simplicity. Clean everything in your App.js file and add the following imports:

import React, { useState } from 'react';
import useSWR from 'swr';
import axios from 'axios';

import './App.css';

Here, we import the useSWR Hook from swr to retrieve cached data records, rather than calling Axios functions directly.

For fetching data without RESTful URL parameters, we typically need to provide two parameters to the useSWR Hook: a unique key (usually the URL) and a fetcher function, which is a JavaScript function that returns asynchronous data.

Add the following code that includes a fetcher:

function fetcher(url) {
  return axios.get(url).then(res => res.data);
}

async function addProduct(product) {
  let response = await axios.post('/products', product);
  return response.data;
}

Here, the fetcher function asynchronously returns data via Axios and the addProduct function similarly posts product data and returns the newly created product.

Now, we can use the useSWR(‘/products’, fetcher) statement in functional components to fetch cached products, but SWR developers recommend using re-usable custom Hooks. Add the following Hook to the App.js file:

function useProducts() {
  const { data, error, mutate } = useSWR('/products', fetcher);
  return {
    products: data,
    isLoading: !data,
    isError: !!error,
    mutate
  };
}

Our useProducts custom Hook outputs the following props:

  • products: An array of products after fetching data from the API; it becomes undefined if no data is available from the API
  • isLoading: Loading indicator based on API data
  • isError: A boolean value to indicate loading errors
  • mutate: A function to update cached data that reflects on UI instantly

Using SWR for data fetching

Now we can use the useProducts data Hook to update the UI from backend data. Create the Products component to list out all of the available products:

function Products() {
  const { products, isLoading, isError } = useProducts();
  if(isError)
    return (
      <div>Unable to fetch products.</div>
    );

  if(isLoading)
    return (
      <div>Loading products...</div>
    );

  return (
    products.map((product) => (
      <div key={product.id} className="product-item">
        <div>{product.name}</div>
        <div>${product.price}</div>
      </div>
    ))
  );
}

The Products component renders conditionally based on the useProducts Hook props. If you use this Hook multiple times in many components, SWR will initiate only one HTTP request, per the request deduplication feature, then, the fetched data will be shared with all components for the rendering process via the useProducts Hook.

Mutating cached data and invalidating requests

Create a component called AddProduct and implement a way to add a new product with the following code:

function AddProduct({ goToList }) {
  const { products, mutate } = useProducts();
  const [product, setProduct] = useState({
    id: products.length + 1,
    name: '',
    price: null
  });
  const [disabled, setDisabled] = useState(true);

  async function handleAdd() {
    goToList();
    mutate(async () => {
      return [...products, await addProduct(product)]
    }, { optimisticData: [...products, product], rollbackOnError: true, revalidate: false } );
  }

  function handleFieldUpdate(e) {
    const element = e.target;
    const value = element.type === 'number' ? parseInt(element.value) : element.value;
    const nextProduct = {...product, [element.name]: value};

    setProduct(nextProduct);
    setDisabled(!nextProduct.name || !nextProduct.price);
  }

  return(
    <div className="product-form">
      <input
        type="text"
        name="name"
        placeholder="Name"
        autoFocus
        onChange={handleFieldUpdate}/>
      <input
        type="number"
        name="price"
        min="1"
        placeholder="Price"
        onChange={handleFieldUpdate}/>
      <button onClick={handleAdd} disabled={disabled}>Add</button>
    </div>
  );
}

Read the mutate function call carefully:

mutate(async () => {
  return [...products, await addProduct(product)]
}, {
    optimisticData: [...products, product],
    rollbackOnError: true,
    revalidate: false
    }
);

Here, we ask SWR to update rendered products directly with the optimisticData option; then, we can use the addProduct function call to insert the specified element into the database. We can also return the updated products list from the async function because our SWR mutation expects updated data records from the async function’s return value.

As the final step, add the exported App component and complete the implementation:

function App() {
  const [ mode, setMode ] = useState('list');
  return (
    <>
    <div className="menu-bar">
      <div onClick={() => { setMode('list') }}
          className={mode === 'list' ? 'selected' : ''}>All products</div>
      <div onClick={() => { setMode('add') }}
          className={mode === 'add' ? 'selected' : ''}>Add product</div>
    </div>
    <div className="wrapper">
      { mode === 'list' ? <Products/> :
          <AddProduct goToList={() => setMode('list')}/> }
    </div>
    </>
  );
}

export default App;

Now run the application:

npm start
# --- or --- 
yarn start

First, study how SWR caches the ProductList component’s data — you will see the loading text only once. Later, you will receive the cached content.

Look at the following preview:

Caching the product list data with SWR
Caching the product list data with SWR

Next, notice how SWR improves usability by directly manipulating the rendered content before updating and re-fetching data in the background within the AddProduct component. Add a new product, and see that the data record is immediately rendered, as shown below:

Updating the cached content before the API call
Updating the cached content before the API call

Finally, SWR comes with some additional features, like re-validation on focus and inspect the network tab to see network calls:

Testing the revalidation-on-focus feature
Testing the revalidation-on-focus feature

What is TanStack Query?

TanStack Query is another open source, full-featured, TypeScript-ready library that offers an API for data fetching and caching in React apps. It implements the library’s agnostic core logic in a separate internal package and offers the React Query adaptor package specifically for React.

TanStack Query for React provides Hooks, classes, and an official, dedicated GUI-based developer tool for syncing client state and server state in React apps. Similarly, the development team plans to offer official adaptor packages for other frontend libraries, i.e., TanStack Vue Query, Svelte Query, etc.



TanStack Query was first released in 2014 via its v0.0.6 public release, about one year after React’s initial release.

Highlighted features

This library offers the following highlighted features:

Feature Description
Batteries-included, framework-like experience TanStack Query offers a framework-like experience for React developers, with a dedicated developer tool, dedicated Hooks for every specific task, OOP classes for better code organization, and JavaScript-props-based event handlers.
Detailed, configurable, and re-usable API TanStack Query strives to provide a detailed, configurable, and full-featured API for fetching and caching remote data within React apps. It offers multiple hooks and classes from its API core for better code organization.
Inbuilt features for developers and users TanStack Query supports paginated requests and provides the useInfiniteQuery Hook to implement infinite loading.

It also offers a React Suspense API, SSG, and SSR support for developers — pre-fetching, re-validation on focus, and network status re-fetching like usability enhancements for app users.

Using TanStack Query

Now that we’ve reviewed the features that TanStack Query offers for optimized data fetching in React, let’s create a sample app and evaluate it to find out comparison points with React SWR.

If you are already a TanStack Query user or you’ve experimented with TanStack Query before, you can check the complete project in this GitHub repository and skip to the comparison section.

First, configure the mock API server and start it as we did in the React SWR section. Now, create another React project to implement the previous simple product management app with TanStack Query:

npx create-react-app tanstack-query-example
cd tanstack-query-example

Installing the package

Install the @tanstack/react-query package with the following command:

npm install @tanstack/react-query
# --- or ---
yarn add @tanstack/react-query

Install the Axios package and define the base URL by following the same steps we did in the SWR section. Get ready to rewrite the previous app with TanStack Query!

Clean everything in the App.js file and add the following imports:

import React, { useState } from 'react';
import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useQueryClient,
  useMutation } from '@tanstack/react-query';
import axios from 'axios';

import './App.css';

Here, the useQuery and useMutation Hooks help with data fetching and updating (cached data). We can use the QueryClient class to create a broker-like instance to access or manipulate cached data. The useQueryClient Hook returns the current QueryClient reference in all app components.

The QueryClientProvider component enables access to cached data for the entire React app, similar to the inbuilt Context.Provider component in the React Context API.

Implementing data fetchers and custom Hooks

Similar to SWR, now we can create a wrapper for Axios, a function to insert a product to the database, and a custom Hook to fetch cached products, as shown below:

function fetcher(url) {
  return axios.get(url).then(res => res.data);
}

async function addProduct(product) {
  let response = await axios.post('/products', product);
  return response.data;
}

function useProducts() {
  const { data, isLoading, error } = useQuery(['products'], () => fetcher('/products'));
  return {
    products: data,
    isLoading,
    isError: !!error
  };
}

Unlike SWR, here, we have the convenient isLoading prop for conditional rendering, but with version 4, we need to send both an array-based, unique key and a URL segment to the useQuery Hook because the Hook calls the fetcher function with a context object — it doesn’t pass the unique key string directly, the way SWR does.

Using TanStack Query for data fetching

We can use the same Products component source from the SWR project, since the custom Hook is almost the same:

function Products() {
  const { products, isLoading, isError } = useProducts();

  if(isError)
    return (
      <div>Unable to fetch products.</div>
    );

  if(isLoading)
    return (
      <div>Loading products...</div>
    );

  return (
    products.map((product) => (
      <div key={product.id} className="product-item">
        <div>{product.name}</div>
        <div>${product.price}</div>
      </div>
    ))
  );
}

We can use the useProducts Hook in multiple components without worrying about RESTful HTTP request duplication issues, since TanStack Query also deduplicates similar requests the way SWR does.

Mutating cached data and invalidating requests

Create a component called AddProduct and implement a way to add a new product with the following code:

function AddProduct({ goToList }) {
  const { products } = useProducts();
  const queryClient = useQueryClient();
  const mutation = useMutation((product) => addProduct(product), {
    onMutate: async (product) => {
      await queryClient.cancelQueries(['products']);

      const previousValue = queryClient.getQueryData(['products']);
      queryClient.setQueryData(['products'], (old) => [...old, product]);
      return previousValue;
    },
    onError: (err, variables, previousValue) =>
      queryClient.setQueryData(['products'], previousValue),
    onSettled: () => queryClient.invalidateQueries(['products'])
  });

  const [product, setProduct] = useState({
    id: products ? products.length + 1 : 0,
    name: '',
    price: null
  });
  const [disabled, setDisabled] = useState(true);

  async function handleAdd() {
    setTimeout(goToList);
    mutation.mutate(product);
  }

  function handleFieldUpdate(e) {
    const element = e.target;
    const value = element.type === 'number' ? parseInt(element.value) : element.value;
    const nextProduct = {...product, [element.name]: value};

    setProduct(nextProduct);
    setDisabled(!nextProduct.name || !nextProduct.price);
  }

  return(
    <div className="product-form">
      <input
        type="text"
        name="name"
        placeholder="Name"
        autoFocus
        onChange={handleFieldUpdate}/>
      <input
        type="number"
        name="price"
        min="1"
        placeholder="Price"
        onChange={handleFieldUpdate}/>
      <button onClick={handleAdd} disabled={disabled}>Add</button>
    </div>
  );
}

TanStack Query offers a full-featured mutation API that provides transparent access to the entire mutation lifecycle. As you can see, we have onMutate, onError, and onSettled callbacks to implement our mutation strategy.


More great articles from LogRocket:


In this example, we update the cached data directly with the new product object, then let TanStack Query send a request to the POST endpoint to update the server state in the background.

SWR offers the mutation strategy as an inbuilt feature with limited customization support, but this isn’t a dealbreaker, as SWR’s fixed mutation strategy solves almost all developers’ needs. However, TanStack Query lets you implement a mutation strategy as you wish, unlike SWR.

Let’s create a new query client for the App component:

const queryClient = new QueryClient();

A query client instance helps provide access to the cached data records in every app component.

Finally, add the exported App component source to your App.js file:

function App() {
  const [ mode, setMode ] = useState('list');
  return (
    <QueryClientProvider client={queryClient}>
      <div className="menu-bar">
        <div onClick={() => { setMode('list') }}
            className={mode === 'list' ? 'selected' : ''}>All products</div>
        <div onClick={() => { setMode('add') }}
            className={mode === 'add' ? 'selected' : ''}>Add product</div>
      </div>
      <div className="wrapper">
        { mode === 'list' ? <Products/> :
            <AddProduct goToList={() => setMode('list')}/> }
      </div>
    </QueryClientProvider>
  );
}

export default App;

We now need to wrap our app components with the QueryClientProvider library component by providing the query client reference to get useQueryClient functioning properly in all child components.

Start the RESTful mock server and run the app — you will see the same app we implemented with SWR. Try to open two tabs and add new products; you will see the re-validation-on-focus feature in action, as we expect.

Now, let’s compare both the SWR and TanStack Query libraries based on the above findings.

SWR vs. TanStack Query

Basic CRUD features

Earlier, we tried data retrieval and manipulation (fetching and mutation) to test CRUD support in both caching libraries. Both SWR and TanStack Query offer the features required to implement the sample app.

SWR strives to give every feature in a minimal way, which may motivate developers to write less code for data caching-related activities. But a minimal API design can sometimes come with limitations for in-depth customization. TanStack Query provides basic fetching and mutation features in a more customizable way than SWR, while SWR offers similar features in a more minimal way than TanStack Query.

Both libraries are backend-agnostic with a promise-based fetcher function, so you can use both SWR and TanStack Query with REST, GraphQL, or any other communication mechanisms with preferred libraries you like: Axios, Unfetch, graphql-request, etc.

Overall, both libraries should satisfy developers’ requirements for basic fetching and mutation support.

Popularity and developer support

An open source library typically becomes popular and gains GitHub stargazers in a few situations:

  • When more developers use the specific library
  • When that library provides better-than-average developer support
  • When their repositories are well-maintained

Both of these libraries have many GitHub stargazers. Both libraries also have great developer communities — developers help each other by answering support queries on the GitHub repositories of these libraries. React SWR doesn’t offer an official developer tool for debugging, but a community member created a GUI developer tool for debugging purposes.

TanStack Query maintains more detailed, well-organized, and supportive official documentation than SWR. However, both libraries offer excellent example projects/code snippets for developers to quickly understand their basic concepts.

Developer tools

A dedicated GUI debugging tool is not mandatory for a third-party library. Still, a topic like caching is indeed complex, so a developer tool for a caching library can really save development time.

TanStack Query comes with an official developer tool, but SWR’s developer community created a non-official but well-used swr-devtools for SWR.

swr-devtools is minimal and only shows you read-only data, but it does include the crucial information you need for debugging:

The SWR DevTools community GUI displays past queries
The SWR DevTools community GUI displays past queries

The TanStack Query developer tool shows you the cached data and lets you manipulate the cached content, unlike the read-only swr-devtools:

The TanStack Query DevTools GUI displays current queries
The TanStack Query DevTools GUI displays current queries

According to the GitHub issue tracker, the swr-devtools project is planning to add support for cache manipulation into the developer tools panel.

Inbuilt usability features

There are three key reasons to use a library like SWR or TanStack Query:

  1. Reduce the amount of code you need to write to sync server-state and React app-state
  2. Use remote resources optimally via data caching and deduplicated queries, like concepts
  3. Improve application usability with a real-time experience

Usability improvement is a crucial reason for caching and query optimization, so both libraries competitively provide the following usability features:

  • Re-validation on focus
  • Network status re-fetching
  • Data pre-fetching
  • Revalidation based on a time interval

TanStack Query provides the following additional usability features:

  • Scroll restoration for saving the infinite scroll position when the user returns to the component again
  • Query cancellation to stop long-running queries
  • Offline mutation support

Bundle size and performance optimizations

Not all users have super-fast internet connections or use high-end computers. Therefore, maintaining a healthy bundle size and implementing performance optimizations help all users run your app smoothly, regardless of their internet speed and computer specifications. It’s a good practice to consume the optimal hardware resources from the user’s computer for web applications.

React SWR is a very lightweight library: BundlePhobia measures its gzipped size as only 4.2kB. TanStack Query is a bit heavy due to its extensive features, so it is 11.4 kB gzipped. It is indeed more than four times the size of React’s core library!

Both libraries do render optimizations, request deduplication, and cache optimizations internally. Note that a caching library wouldn’ boost HTTP request handling speed — HTTP request performance depends on various factors, such as the HTTP client library performance, the browser’s JavaScript engine implementation, network speed, current CPU load, etc.

SWR vs. TanStack Query: Summary

Let’s summarize the above comparison factors in one table. Look at the following table and compare SWR and TanStack Query side-by-side:

Comparison factor React SWR TanStack Query
Overall API design Provides a minimal API for developers with some fixed features Provides a detailed and somewhat complex API for developers with fully customizable features
Bundle size (gzipped) 4.2 KB 11.4 KB
Popularity, community support, and documentation Good community, a well-maintained repository, and overall good documentation with demos Good community, well-maintained repository, and informative documentation with many practical examples and complete API reference
Basic data fetching and mutation features Satisfies developer requirements, but the developer has to write additional code for some features and may face in-depth customization issues Satisfies developer requirements with in-depth customization support. Developers who try to integrate it with smaller projects may find the API a bit more complex than it should be
Performance optimizations Supports request deduplication, render optimizations, and optimized caching Supports request deduplication, render optimizations, and optimized caching
Inbuilt usability features Revalidation on focus, network status re-fetching, data pre-fetching, and re-validation based on an interval Revalidation on focus, network status re-fetching, data pre-fetching, revalidation based on an interval, request cancellation, offline mutation, and scroll restoration
Inbuilt features for developers Offers pagination and infinite loading features. The developer community implemented a developer tool GUI with Chrome and Firefox extensions. Supports persisting cache into external storage locations (i.e., localStorage). Offers pagination and infinite loading features. It comes with an official developer tool GUI with cache manipulation support. Supports persisting cache into external storage locations (i.e., localStorage).
React Suspense Supported Supported
Official support for other frontend libraries No, similar community libraries available: sswr In progress, similar community libraries available: vue-query

Conclusion

In this article, we created a sample React application with both SWR and TanStack Query libraries, then we compared them according to the developer experience and available features.

Both libraries competitively perform better and have various pros and cons, as we’ve outlined here. React SWR’s goal is to provide a minimal API to solve the caching problem in React request handling by maintaining a lightweight library. Meanwhile, TanStack Query strives to offer a fully featured solution for the same problem.

Sometimes, TanStack Query looks like a framework that provides everything you need from one development kit , like Angular — SWR, on the other hand, looks more like React in that it focuses on solving only one problem. React introduced functional components to reduce complexity in class-based components, so developers who like that kind of simplicity may prefer SWR over TanStack Query.

Developers who love to work with detailed/robust APIs and seek framework-like, all-in-one solutions for data caching may choose TanStack Query over SWR. The TanStack Query team is planning to offer official support for Svelte, SolidJS, Vue.js, and vanilla JavaScript apps with adaptor libraries for the core TanStack Query library. However, the frontend developer community has already implemented several open-source caching libraries according to TanStack Query and React SWR APIs for other frontend frameworks.

Our conclusion? Try both libraries. Select one according to your API preferences. TanStack Query has several unique features and in-depth customizations that SWR doesn’t support at the time of writing this article. It’s likely both libraries will become equally fully featured and robust, especially if SWR commits to implementing some missing functionalities in the near future.

However, from the minimal API design perspective, SWR is already complete and offers the mandatory features you seek without increasing the bundle size further, which TanStack Query certainly will do.

Cut through the noise of traditional React error reporting with LogRocket

LogRocket is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications.

LogRocket automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — .

Shalitha Suranga Programmer | Author of Neutralino.js | Technical Writer

Leave a Reply