Written by Elijah Agbonze✏️
When it comes to handling loading states, error handling, revalidating, prefetching, and managing multiple calls, using useEffect
can be overwhelming. This has been the dilemma of client-side data fetching and handling where you either have to set up a state management system or settle for the useEffect
Hook.
But with SWR, fetching and handling client-side data has never been easier. SWR, which stands for Stale-While-Revalidate, is a powerful library for data fetching, revalidating, and caching in React applications. The "Stale-While-Revalidate" concept means that while SWR is revalidating the data, it serves the stale data from the cache. This approach strikes the perfect balance between a responsive UI performance and ensuring your users always have access to up-to-date data.
In this article, we are going to look at how we can use SWR to handle data with interactive examples. We will also take a look at some important features and supports of SWR.
Jump ahead:
- Basics of the
useSWR
Hook - Pagination with
useSWR
- Reusable data fetching with SWR: Best practices
- Caching with SWR
- SSG/ISR/SSR support
- Type safety with
useSWR
- Mutation and revalidation
- Conditional fetching with
useSWR
- Error handling strategies with
useSWR
-
useSWR
with GraphQL -
useSWR
performance optimization - Testing strategies
Basics of the useSWR
Hook
const { data, isLoading, error } = useSWR(key, fetcher, options);
useSWR
params
The useSWR
Hook accepts three parameters. The first two are required, and they determine what is going to be fetched and how it will be fetched. The third parameter is an object of options, which lets you tweak the way the data is handled.
The key
param represents the URL, in the case of REST APIs, to which the request is to be made. The reason for the strange key
as name is because it actually is a key. It is a unique string and in some cases, can be a function, object, or an array. For example, during global mutations, the key of a useSWR
Hook is used to make changes specific to that hook.
The fetcher
param is a function that returns a promise of the fetched data:
const fetcher = (url) => fetch(url).then((res) => res.json());
The fetcher
function is not limited to the Fetch API; you can also use Axios for REST APIs and the graphql-request library for GraphQL. We will take a closer look at fetching GraphQL data later in this article:
const { data } = useSWR(key, fetcher);
By default, the only argument passed into the fetcher
function from the example above is the key
param of SWR.
Let's say we have more arguments to pass to the fetcher
function. For example, passing a limit for fetching a list of limited comments from JSONPlaceholder, assuming the limit is triggered by the user on the app, may tempt us to do something like this:
const fetcher = async (url, limit) => {
const res = await fetch(`${url}?_limit=${limit}`);
};
const { data: comments } = useSWR(
`https://jsonplaceholder.typicode.com/comments`,
(url) => fetcher(url, limit)
);
This isn't accurate because the cached key, which is https://jsonplaceholder.typicode.com/comments
, does not include limit
, and if limit
changes, SWR would still use the same cached key and return the wrong data.
In cases like this, where passing multiple arguments to the fetcher
function is paramount, you’d have to pass these arguments to the key
param as either an array or an object:
const fetcher = async ([url, limit]) => {
const res = await fetch(`${url}?_limit=${limit}`);
};
const { data: comments } = useSWR(
[`https://jsonplaceholder.typicode.com/comments`, limit],
fetcher
);
In the example above, both url
and limit
are now part of the key
param. Now, changes to limit
will cause SWR to use a different cached key and provide the accurate data. The same thing applies with using an object.
With the options
param, you can make changes to the way your fetched data is being handled. This includes determining when your data should revalidate if need be, if there should there be a retry if an error occurred while fetching the data, what happens if it was successful, and what happens if it fails. Below are some of the options
properties:
-
revalidateOnFocus
: Automatically revalidates when a user refocuses on the window. Default istrue
-
onSuccess(data, key, config)
: Callback function when a request is successful, and if the request returns an error, theonError
callback function can be used. -
shouldRetryOnError
: Turns on or off retrying when an error occurs. Default istrue
. If set totrue
, you can also determine the max retry count with theerrorRetryCount
option and the retry interval with theerrorRetryInterval
option.
Here is the full list of useSWR
options
param.
useSWR
return values
The useSWR
Hook returns five values:
const { data, error, isLoading, isValidating, mutate } = useSWR(
"/api/somewhere",
fetcher
);
-
data
: The data returned from the request to the given key. It returnsundefined
if not loaded -
error
: Any error thrown in thefetcher
function. It could be a syntax, reference, or even error from the API. We will take a look at how we can throw errors later in this article -
isLoading
: Returnstrue
/false
if there's an ongoing request and no loaded data -
isValidating
: Returnstrue
/false
if there's an ongoing request or revalidation is loading. It is almost similar toisLoading
. The only difference is thatisValidating
also triggers for previously loaded data -
mutate
: A function for making changes to the cached data specific to this cached key only
Let's take a look at an example that uses some of the things we've seen so far. In this simple example, we will fetch comments from JSONPlaceholder. First, create a new Next.js project and paste the code below in pages.jsx
:
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Home() {
const {
data: comments,
isLoading,
isError: error,
} = useSWR(
"https://jsonplaceholder.typicode.com/comments?_limit=6",
fetcher,
{ revalidateOnFocus: false, revalidateOnReconnect: false }
);
if (error) {
return <p>Failed to fetch</p>;
}
if (isLoading) {
return <p>Loading comments...</p>;
}
return (
<ul>
{comments.map((comment, index) => (
<li key={index}>
{comment.name}
</li>
))}
</ul>
);
}
From the example above, notice we turned off revalidation. This is because we’re fetching an immutable data, so we can turn off revalidatio to prevent unnecessary request calls.
Pagination with useSWR
SWR comes with built-in support for handling pagination easily. There are two common pagination UI: the numbered pagination UI and the infinite loading UI: The numbered pagination UI is straightforward with useSWR
:
export default function Home() {
const [pageIndex, setPageIndex] = useState(1);
const { data: comments } = useSWR(
`https://jsonplaceholder.typicode.com/comments?_page=${pageIndex}&_limit=6`,
fetcher
);
return (
<>
{/** ... code for mapping through the comments **/}
<button onClick={() => setPageIndex((_currentPage) => _currentPage - 1)}>
Prev
</button>
<button onClick={() => setPageIndex((_currentPage) => _currentPage + 1)}>
Next
</button>
</>
);
}
As seen above, the pageIndex
state changes the cached key when it is changed. This will cause SWR to request for new data from the API. Here is a CodeSandbox to try out this example.
With SWR's cache, we can preload the next page. All we have to do is create an abstraction for the next page and render it in a hidden div. This way the next page is already loaded and ready to be displayed:
const Page = ({ index }) => {
const { data: comments } = useSWR(
`https://jsonplaceholder.typicode.com/comments?_page=${index}&_limit=6`,
fetcher
);
return (
<ul>
{comments.map((comment, index) => (
<li key={index}>{comment.name}</li>
))}
</ul>
);
};
export default function Home() {
const [pageIndex, setPageIndex] = useState(1);
return (
<>
<Page index={pageIndex} /> {/** for the current displaying page **/}
<div className='hidden'>
<Page index={pageIndex + 1} />
</div>{' '}
{/** for the next page**/}
<div>
<button
onClick={() => setPageIndex((_currentPage) => _currentPage - 1)}
>
Prev
</button>
<button
onClick={() => setPageIndex((_currentPage) => _currentPage + 1)}
>
Next
</button>
</div>
</>
);
}
SWR also provides a different hook for handling infinite loading:
import useSWRInfinite from 'swr/infinite';
const getKey = (pageIndex, previousPageData) => {
// return a falsy value if this is the last page
if (pageIndex && !previousPageData.length) return null;
return `https://jsonplaceholder.typicode.com/comments?_page=${pageIndex}&_limit=6`;
};
export default function Home() {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher);
return (
<>
<ul>
{data.map((comments) => {
return comments.map((comment, index) => (
<li key={index}>{comment.name}</li>
));
})}
</ul>
<button onClick={() => setSize(size + 1)}>Load more</button>
</>
);
}
Here is this example in CodeSandbox. Similar to the useSWR
Hook, useSWRInfinite
takes in a getKey
function that returns the request key, as well as a fetcher
function. It returns an extra size
value along with a function setSize
to update the size
value.
By default, the useSWRInfinite
Hook fetches data for each page in sequence. This means the key for the next page depends on the previous one. This is often useful when the data for fetching the next page depends on the current one.
However, if you don't want to fetch the pages sequentially, then you can turn it off by setting the parallel
option of useSWRInfinite
to true
:
const { data, size, setSize } = useSWRInfinite(getKey, fetcher, {
parallel: true,
});
Reusable data fetching with SWR: Best practices
When fetching similar data across your application, it is best to make fetching the data reusable to avoid duplicate code, and have better code efficiency and maintainability. Assuming we have a dashboard where the current user is displayed across multiple components in our UI, usually you'd want to fetch the user in the top-level component and pass it down to all components using props or React Context. But with SWR, it becomes easier and more efficient:
const useUser = () => {
const { data, isLoading, error } = useSWR(`/api/user`, fetcher);
return { user: data, isLoading, error };
};
Now we can use the hook above across multiple components:
const Home = () => {
const { user } = useUser();
return (
<header>
<h2>Welcome back {user.username}</h2>
<NavBar />
</header>
);
};
const UpdateProfile = () => {
const { user } = useUser();
return (
<header>
<h2>Updating {user.username}</h2>
<NavBar />
</header>
);
};
const NavBar = () => {
const { user } = useUser();
return (
<div>
<img src={user.avatar} /> {user.username};
</div>
);
};
It doesn't matter how many times you reuse the useUser
Hook, the request will only be sent once. This is because all instances of the hook use the same key, so the request is automatically cached and shared.
Reusable data fetching with SWR also helps fetch data in the declarative way, which means you only need to specify what data is used by the component.
Caching with SWR
By default, SWR uses a global cache to store and share data across all your components. This global cache is an instance object that is only created when the app is initialized, and destroyed when the app exits. This is why it is primarily used for short-term caching and fast data retrieval.
If the default cache of SWR is not satisfying for your app, you can make use of a custom cache provider like window's localStorage property, or IndexedDB. This will help you store larger amounts of data and persist it across page reloads. This means exiting the app/browser will not destroy the stored data.
A cache provider is map-like, which means you can directly use the JavaScript Map instance as the cache provider for SWR. You can create custom cache providers on the SWRConfig
component:
import { SWRConfig } from "swr";
const App = () => {
return (
<SWRConfig value={{ provider: () => new Map() }}>{/** app **/}</SWRConfig>
);
};
The custom provider defined above is similar to the default provider of SWR. All SWR hooks under the SWRConfig
component will make use of the defined provider.
You can access the current cache provider of a component with the useSWRConfig
Hook. Along with the cache
property is the mutate
property for modifying the cache directly:
import { useSWRConfig } from "swr";
const App = () => {
const { data: user } = useSWR("/api/user");
const { cache, mutate } = useSWRConfig();
useEffect(() => {
console.log(cache);
}, [cache]);
const updateName = async () => {
const newName = user.name.toUpperCase();
await updateNameInDB(newName);
mutate("/api/user", { ...user, name: newName });
};
return (
<div>
<p>{user.name}</p>
<button onClick={updateName}>Update name</button>
</div>
);
};
You should never update the default cache directly. Instead, use the mutate
function to make updates to the cache directly. Now let's take a look at using a more persistent cache storage using localStorage
.
SWR has a set of standard types for creating a custom cache provider (i.e, the get
, set
, delete
, and keys
methods):
const cacheGet = (key) => {
const cachedData = localStorage.getItem(key);
if (cachedData) {
return JSON.parse(cachedData);
}
return null;
};
const cacheSet = (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
};
const cacheDelete = (key) => {
localStorage.removeItem(key);
};
const cacheKeys = () => {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
keys.push(localStorage.key(i));
}
return keys;
};
export { cacheGet, cacheSet, cacheDelete, cacheKeys };
Now we can define each of these methods in the provider
function in SWRConfig
:
const App = () => {
return (
<SWRConfig
value={{
provider: () => ({
get: cacheGet,
set: cacheSet,
keys: cacheKeys,
delete: cacheDelete,
}),
}}
>
{/** app **/}
</SWRConfig>
);
};
Any useSWR
request within the SWRConfig
above will be stored in the localStorage
. This means you can access your cached data after reloading, and even when offline.
If you don't want to define all the methods for your project, and want something similar to the default cache while still storing in a more persistent memory like localStorage
, all you have to do is sync your cache with localStorage
, and you’ll still be able to access it offline:
const localStorageProvider = () => {
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
console.log(map);
window.addEventListener("beforeunload", () => {
const appCache = JSON.stringify(Array.from(map.entries()));
localStorage.setItem("app-cache", appCache);
});
return map;
};
const App = () => {
return (
<SWRConfig
value={{
provider: localStorageProvider,
}}
>
{/** app **/}
</SWRConfig>
);
};
Notice we're still making use of the JavaScript Map
instance, so we don't have to define the standard methods for an SWR cache provider.
SSG/ISR/SSR support
Having a statically generated blog improves search engine performance and load times. Despite pre-rendering, client-side data fetching is still essential for real-time views, comments, authentication, and data revalidation.
Let’s take a look at pre-rendering in Next.js, which can be done with SSG, ISR, or SSR, and fetching client-side data with SWR.
SSG (Static Site Generation) allows you to generate static pages at build time. Below is an example of a pre-rendered blog post where SWR is used to revalidate the number of views:
const Post = () => {
const { data: post, mutate } = useSWR(
"/api/post",
(url) => fetch(url).then((res) => res.json()),
{ refreshInterval: 1000 }
);
return (
<>
<p>
Views: {post.views}
</p>
<button
onClick={async () => {
await fetch("/api/post", {
method: "put",
});
mutate();
}}
>
Increment views
</button>
</>
);
};
const Index = ({ fallback }) => {
return (
<SWRConfig value={{ fallback }}>
<Post />
</SWRConfig>
);
};
export default Index;
export async function getStaticProps() {
const res = await fetch(`/api/post`);
const post = await res.json();
return {
props: {
fallback: {
"/api/post": post,
},
},
};
}
In the example above, I made use of refreshInterval
to tell useSWR
how often it should revalidate.
ISR (Incremental Static Regeneration) is a extension of SSG. It provides a revalidate
property that is used to specify how often (in seconds) the page should be regenerated. All we have to do is simply add revalidate
to the returned value from getStaticProp
:
export async function getStaticProps() {
const res = await fetch(`/api/post`);
const post = await res.json();
return {
props: {
fallback: {
"api/post": post,
},
},
revalidate: 10,
};
}
We also had to remove the refreshInterval
prop from the SSG example because ISR will incrementally regenerate this page at runtime based on the revalidate
property and when it is requested. So whenever a new user visits the post page, they will have access to the updated views. View the full code and demo of this example.
SSR (Server Side Rendering) generates a page on the server side at runtime. This means that the contents of the page are always up-to-date with the latest data because it is generated at the time of the request. If we were to adjust the examples above to SSR, we'd have this:
export default function Index({ post: initialPost }) {
const { data: post } = useSWR(
'/api/post',
(url) => fetch(url).then((res) => res.json()),
{ fallbackData: { initialPost } }
);
return (
<>
<h2>{post.title}</h2>
</>
);
}
export async function getServerSideProps() {
const res = await fetch(`/api/post`);
const post = await res.json();
return {
props: {
post,
},
};
}
initialPost
is the post fetched on the server side with getServerSideProps
. When the page is requested, it will render the up-to-date views. You can turn all revalidation from SWR off to see the dynamic rendering at work. View the demo of this example.
Type safety with useSWR
SWR supports TypeScript out of the box. Recall that when specifying multiple arguments for the fetcher
function, we usually pass it to the key
param. Now for specifying the types of these arguments, SWR will infer the types of fetcher
from key
:
// url will be inferred as a string, while limit will be inferred as number
const { data: comments } = useSWR(
[`https://jsonplaceholder.typicode.com/comments`, 2],
([url, limit]) => fetch(`${url}?_limit=${limit}`).then((res) => res.json())
);
If you're not happy with inferred types, you can explicitly specify the types you want:
import useSWR, { Fetcher } from "swr";
const fetcher: Fetcher<Comment, string> = fetch(url).then((res) => res.json());
const { data: comments } = useSWR(
`https://jsonplaceholder.typicode.com/comments`,
fetcher
);
Mutation and revalidation
Mutation is simply for updating the data of a cached key, while revalidating whenever/however triggered re-fetches a cached key.
There are two ways of using mutation. The first way is by using the global mutate
function, which can mutate any cached key:
import { useSWRConfig } from "swr";
const App = () => {
const { data: user } = useSWR("/api/user");
const { cache, mutate } = useSWRConfig();
const updateName = async () => {
const newName = user.name.toUpperCase();
await updateNameInDB(newName);
mutate("/api/user", { ...user, name: newName });
};
return (
<div>
<p>{user.name}</p>
<button onClick={updateName}>Update name</button>
</div>
);
};
mutate
in the example above is a global mutate because it's from the useSWRConfig
. We could also import the global mutate
from swr
:
import { mutate } from "swr";
const App = () => {
const { data: user } = useSWR("/api/user");
const updateName = async () => {
const newName = user.name.toUpperCase();
await updateNameInDB(newName);
mutate("/api/user", { ...user, name: newName });
};
return (
<div>
<p>{user.name}</p>
<button onClick={updateName}>Update name</button>
</div>
);
};
The first param is the key
of the useSWR
Hook you want to mutate. The second is for passing the updated data.
The second way of using mutation is the bound mutate
, which only mutates its corresponding useSWR
Hook:
const App = () => {
const { data: user, mutate } = useSWR("/api/user");
const updateName = async () => {
const newName = user.name.toUpperCase();
await updateNameInDB(newName);
mutate({ ...user, name: newName }); // no need to specify the key, as the key is already known
};
return (
<div>
<p>{user.name}</p>
<button onClick={updateName}>Update name</button>
</div>
);
};
Revalidation
Calling mutate
with key
will trigger a revalidation for all useSWR
hooks that uses that key. But without a key
, it will only revalidate its corresponding useSWR
Hook:
const App = () => {
const { data: user, mutate } = useSWR("/api/user");
const updateName = async () => {
const newName = user.name.toUpperCase();
await updateNameInDB(newName);
mutate();
};
return (
<div>
<p>{user.name}</p>
<button onClick={updateName}>Update name</button>
</div>
);
};
In the example above, we didn't have to specify a cached data because the mutate
function will trigger a re-fetch and that will cause the updated data to be displayed and cached. While this sounds cool, it can be a problem when you're re-fetching a large amount of data when you only wanted the new data to be added. Here is an example.
In cases where the API you're writing to returns the updated/new value, we can simply update the cache after mutation:
const addPost = async () => {
if (!postTitle || !postBody) return;
const addNewPost = async () => {
const res = await fetch("/api/post/new", {
method: "post",
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify({
title: postTitle,
body: postBody,
id: posts.length + 1,
}),
});
return await res.json();
};
mutate(addNewPost, {
populateCache: (newPost, posts) => {
return [...posts, newPost];
},
revalidate: false,
});
};
Here is the CodeSandbox for the complete example. In the example above, notice that we changed the revalidate
property to false
. This will prevent the SWR from re-fetching the whole data when mutate
is used.
Like what we've seen above, revalidating can sometimes be redundant and unnecessary. To stop these unnecessary requests, you can turn off all automatic revalidations with useSWRImmutable
:
import useSWRImmutable from "swr/immutable";
const App = () => {
const { data: user, mutate } = useSWRImmutable("/api/user");
return (
<div>
<p>{user.name}</p>
</div>
);
};
It's the same as useSWR
, with the only difference being that all automatic revalidation options such as revalidateIfStale
, revalidateOnFocus
, and revalidateOnReconnect
are set to false
automatically.
Conditional fetching with useSWR
The key
param can also be a function, as long as it returns a string, array, object, or a falsy value. When fetching data that depends on another, you can either pass a function to the key
param or simply use a ternary operator directly, like so:
const fetcher = (url) => fetch(url).then((res) => res.json());
const { data: posts } = useSWR(
"https://jsonplaceholder.typicode.com/posts?_limit=20",
fetcher
);
// using a ternary operator directly
const { data: comments } = useSWR(
posts.length >= 1
? `https://jsonplaceholder.typicode.com/posts/${posts[12].id}/comments?_limit=6`
: null,
fetcher
);
// using a function
const { data: comments } = useSWR(
() =>
posts.length >= 1
? `https://jsonplaceholder.typicode.com/posts/${posts[12].id}/comments?_limit=6`
: null,
fetcher
);
Here is the example above in CodeSandbox. When a falsy value is passed to the key
param, it will return as an error by the useSWR
Hook.
Error handling strategies with useSWR
As a developer, when an error is thrown when fetching data, you can either decide to display the error, or try fetching again. Let’s take a look at handling these options:
Displaying errors
One benefit of using useSWR
is that it returns errors specific to a component, and right there you can display what went wrong. We've seen example of this throughout this article, now let’s see how we can send customized messages based on the error:
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) {
const errorRes = await res.json();
const error = new Error();
error.info = errorRes;
error.status = res.status;
error.message = "An error occurred while fetching data";
throw error;
}
return await res.json();
};
Here is the complete example on CodeSandbox. You can also display an error globally with the SWRConfig
component.
Retrying on error
SWR provides an onErrorRetry
callback function as part of the useSWR
options. It uses the exponential backoff algorithm to retry the request exponentially on error. This prevents incessant retrying. The callback allows you to control retrying errors based on conditions:
useSWR("/api/user", fetcher, {
onErrorRetry: (error, key, config, { retryCount }) => {
// Never retry on 404.
if (error.status === 404) return;
// Only retry up to 10 times.
if (retryCount >= 10) return;
},
});
useSWR
with GraphQL
Fetching data from a GraphQL API with useSWR
is similar to fetching from a REST API. The only difference is that you need a third-party library to make a request to the GraphQL API. A popular library is the graphql-request package:
import request from 'graphql-request';
import useSWR from 'swr';
const graphQLFetcher = (query) => request('/graphql', query);
const Books = () => {
const {
data,
error: isError,
loading: isLoading,
} = useSWR(
`
{
books {
name
id
}
}
`,
graphQLFetcher
);
if (isError) return <div>Error loading books</div>;
if (isLoading) return <div>Loading books...</div>;
return (
<ul>
{data?.books.map((book) => (
<li key={book.id}>{book.name}</li>
))}
</ul>
);
};
export default Books;
Here is the demo of the example above.
Mutating a GraphQL API is the same thing as what we've seen before. You can either use the global mutate
, bound mutate
, or the useSWRMutation
Hook. Here is an example with bound mutate
.
One useful feature of useSWR
that we haven’t discussed yet is the useSWRSubscription
Hook. This hook is used for subscribing to real-time data. Let's say we have a WebSocket server that sends a new message every second:
// the server
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 5000 });
wss.on("connection", (ws) => {
console.log("Client connected");
let number = 5000;
setInterval(() => {
number -= 1;
ws.send(`Current number is ${number}`);
}, 1000);
ws.on("close", () => {
console.log("Client disconnected");
});
});
console.log("WebSocket server started on port 5000");
Making use of the useSWRSubscription
, we can subscribe to this server and listen for events to the client like so:
import useSWRSubscription from "swr/subscription";
const Countdown = () => {
const { data } = useSWRSubscription(
"ws://localhost:5000",
(key, { next }) => {
const socket = new WebSocket(key);
socket.addEventListener("message", (event) => next(null, event.data));
socket.addEventListener("error", (event) => next(event.error));
return () => socket.close();
}
);
return (
<>
<p>{data}</p>
</>
);
};
export default Countdown;
useSWR
performance optimization
- Caching strategies:
useSWR
allows you to specify caching strategies for fetching data. For example, you can use the "cache-first" strategy to serve data from the cache, thereby making a network request only if data is not available - Prefetching: The
useSWR
mutate
function can be used for data prefetching. Themutate
function allows you to update the cached data without making a new network request, allowing you to pre-fetch data and keep it up-to-date in the cache - Using custom hooks: Encapsulate
useSWR
logic in custom hooks to promote reusable data fetching and make it easier to manage data fetching across your application - Error handling: Display appropriate error messages to users or automatically retry errors
- Declarative fetching:
useSWR
makes it easier to fetch only the data that the current component requires, rather than fetching all data simultaneously. This helps minimize unnecessary network calls - Loading states: Provide loading state feedback to users while data is being fetched
Testing strategies
Testing useSWR
can seem daunting at first, but it's an essential step to ensure your React applications work smoothly. Some of these strategies include:
- Testing data fetching by crafting mock API responses and ensuring your component displays the fetched data as expected
- Testing that the caching behavior works as intended
- Testing mock API responses with errors to ensure your component displays appropriate error messages
- Testing loading states by simulating data fetch delays
- Testing that data is revalidated and updated as expected, especially after the revalidation interval.
- Testing data prefetching using the
mutate
function fromuseSWR
. Verify that the data is indeed prefetched and readily available in the cache when needed
Conclusion
So far, we've seen how easy it is to fetch, prefetch, cache, mutate, and revalidate data regardless of the size with useSWR
. We also looked at how to fetch from two different kinds of API (REST and GraphQL) using custom fetcher functions for them.
Each feature that we discussed is important and can be useful in your day to day client-side data fetching. There are many other features you can check out on the official useSWR
docs. Happy hacking!
LogRocket: Full visibility into production Next.js apps
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Top comments (1)
Thanks for sharing!