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:
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 Stale While Revalidate, a generic caching principle from HTTP RFC 5861.
React SWR was first released in 2019 via its v0.1.2 public release.
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. |
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
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
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 APIisLoading
: Loading indicator based on API dataisError
: A boolean value to indicate loading errorsmutate
: A function to update cached data that reflects on UI instantlyNow 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.
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:
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:
Finally, SWR comes with some additional features, like re-validation on focus and inspect the network tab to see network calls:
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.
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. |
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
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.
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.
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.
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.
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.
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.
An open source library typically becomes popular and gains GitHub stargazers in a few situations:
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.
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 TanStack Query developer tool shows you the cached data and lets you manipulate the cached content, unlike the read-only swr-devtools:
According to the GitHub issue tracker, the swr-devtools project is planning to add support for cache manipulation into the developer tools panel.
There are three key reasons to use a library like SWR or TanStack Query:
Usability improvement is a crucial reason for caching and query optimization, so both libraries competitively provide the following usability features:
TanStack Query provides the following additional usability features:
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.
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 |
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Caching clash: SWR vs. TanStack Query for React"
SWR doesn’t stand for **State** While Re-validate according to linked RFC (section 3) but rather **Stale** While Revalidate (i.e. use the stale content without blocking while checking for updates).