
Ismaïla Bah
Complete Guide: Using useSWR in Next.js App Router
In this article, we’ll dive into using useSWR in a Next.js application using the App Router. We’ll explore how to efficiently handle API requests, the cache, and how to update data without revalidating the cache unnecessarily.
Introduction to SWR
SWR (stale-while-revalidate) is a data management hook built for React that allows you to automatically fetch, cache, and synchronize data. It simplifies API request management while optimizing the performance of your applications.
Why use SWR?
• Automatic Cache: Once data is retrieved, SWR caches it and makes it available without the need to make API requests each time.
• Automatic Revalidation: When a page regains focus or when the connection is reestablished, SWR automatically revalidates the data in the background.
• Error Handling: SWR handles errors cleanly, with automatic retry options
Implementing SWR in a Next.js application
SWR integrates seamlessly with Next.js, and in the context of the App Router, it is mainly used in components of your pages or layouts.
Installation
1npm install swr
Basic Syntax
The useSWR hook has a simple syntax that is broken down into three parts:
1const { data, error, isLoading } = useSWR(key, fetcher, options);
• key: The unique key used to identify and cache the data (usually the API URL).
• fetcher: The function that fetches the data (like fetch or axios).
• options: Additional options to configure the behavior of SWR.
Here is a basic example where we use fetch to retrieve data from an API.
1import axios from 'axios';23const fetcher = (url) => axios.get(url).then((res) => res.data);45const MyComponent = () => {6 const { data, error, isLoading } = useSWR('/api/posts', fetcher);78 if (isLoading) return <p>Chargement...</p>;9 if (error) return <p>Erreur : {error.message}</p>;1011 return (12 <div>13 {data.map((post) => (14 <p key={post.id}>{post.title}</p>15 ))}16 </div>17 );18};
In this example, SWR fetches data from the URL /api/posts, caches it, and makes it available in the component.
SWR Features and Options
SWR offers many options to control how data is retrieved and managed. Let’s explore these options in more detail.
suspense
• By default: suspense is off (false), but you can enable it to use React's Suspense mode.
• How it works: When you enable Suspense, SWR suspends rendering of your component until the data is fetched.
Example :
1const { data } = useSWR('/api/posts', fetcher, {2 suspense: true3});
Explanation: The component will wait for data to load before rendering. This needs to be combined with React's <Suspense> component to handle the loading state.
fetcher(args)
• Role: This is the function that is called to retrieve the data.
Example :
1import axios from 'axios';23const fetcher = (url) => axios.get(url).then((res) => res.data);
Explanation: fetcher is simply the function that sends the HTTP request. You can use fetch, axios, or any other data retrieval method.
revalidateIfStale
• Default: If cached data is stale, it will be revalidated automatically.
Example :
1const { data } = useSWR('/api/posts', fetcher, {2 revalidateIfStale: false3});
Explanation: With revalidateIfStale disabled (false), SWR will not revalidate data if it is present in cache, even if it is considered stale.
revalidateOnMount
• Operation: This option controls whether the data should be revalidated when the component is mounted.
• Default: This option is enabled, which means that when the component mounts, SWR checks if the cached data is up to date.
Example :
1const { data } = useSWR('/api/posts', fetcher, {2 revalidateOnMount: false3});
Explanation: With revalidateOnMount: false, SWR will not perform a new request when mounting the component if the data already exists in cache.
revalidateOnFocus
• Default: SWR revalidates data when the user returns to the tab or window (loses and regains focus).
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 revalidateOnFocus: false3});
Explanation: Disabling this option prevents automatic revalidation when the user returns to the page after leaving it.
revalidateOnReconnect
Default: SWR revalidates data when network connection is restored (via navigator.onLine).
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 revalidateOnReconnect: false3});
Explanation: With revalidateOnReconnect: false, SWR does not automatically retry the request when the user regains a network connection after losing it.
refreshInterval
How it works: Sets a refresh interval (in milliseconds) to revalidate data periodically.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 refreshInterval: 50003});
Explanation: Here, the data is revalidated every 5 seconds (5000 ms). You can also define a function to adjust the interval based on the data.
refreshWhenHidden
Operation: Enables or disables data refreshing when the window is not visible.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 refreshWhenHidden: true,3 refreshInterval: 50004});
Explanation: If enabled, SWR will continue to revalidate data every 5 seconds, even when the window is minimized or the tab is in the background.
refreshWhenOffline
How it works: Enables or disables data refreshing even when the user is offline.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 refreshWhenOffline: true,3 refreshInterval: 50004});
Explanation: This allows to run revalidation even in offline mode (which is only useful if you have a specific offline recovery strategy).
shouldRetryOnError
• Operation: Indicates whether SWR should retry the request after an error.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 shouldRetryOnError: false3});
Explanation: If disabled, SWR will not retry to recover data after an error.
dedupingInterval
• Default: The value is 2000 milliseconds (2 seconds)
• Operation: Interval (in milliseconds) during which requests with the same key are deduplicated.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 dedupingInterval: 50003});
Explanation: During this period, repeated requests with the same key are not executed (to avoid duplicating network calls).
Nb: Difference between refreshInterval and dedupingInterval
• refreshInterval: This is to refresh the data regularly, even if the user does nothing.
• dedupingInterval: This is to prevent multiple identical queries from being executed in a short period of time (for example, due to a remount or repeated user action).
focusThrottleInterval
• Default: Value is 5000 milliseconds (5 seconds)
• Operation: Interval (in milliseconds) during which SWR ignores multiple consecutive revalidations due to window focus.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 focusThrottleInterval: 100003});
Explanation: This limits the frequency of automatic revalidations when returning to the tab/window frequently. Here, a revalidation is not done more than once every 10 seconds.
loadingTimeout
• Default: Value is 3000 milliseconds (3 seconds)
• How it works: Maximum time before considering a request as slow (in milliseconds). Triggers the onLoadingSlow option if loading takes longer.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 loadingTimeout: 5000,3 onLoadingSlow: (key, config) => {4 console.log(`Loading for ${key} is slow.`);5 }6})
Explanation: If the data takes more than 5 seconds to load, the onLoadingSlow callback is executed.
errorRetryInterval
• Operation: Delay (in milliseconds) between each retry attempt after an error.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 errorRetryInterval: 100003});
Explanation: SWR waits 10 seconds before retrying to retrieve data in case of error.
errorRetryCount
• How it works: Sets the maximum number of retry attempts in case of a query error.
• Default: SWR will retry indefinitely as long as shouldRetryOnError is enabled.
Example:
1const { data, error } = useSWR('/api/posts', fetcher, {2 errorRetryCount: 33});
Explanation: Here, after 3 unsuccessful attempts, SWR stops retrying to fetch the data.
fallback
• How it works: This is a key-value object that contains fallback data to use if queries fail or data is not loaded yet.
Example:
1const fallback = { '/api/posts': [{ title: 'Fallback Post' }] };2const { data } = useSWR('/api/posts', fetcher, { fallback });
Explanation: Here, if the data is not yet available, SWR uses the data provided in fallback as a fallback until the real response arrives.
fallbackData
• How it works: A similar option to fallback, but specific to each hook. It allows to provide initial data before the request is completed.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 fallbackData: [{ title: 'Initial Data' }]3});
Explanation: Before data is loaded, SWR returns fallbackData as if it were already available.
keepPreviousData = false
• How it works: If enabled, SWR will keep previous data until new data is loaded, instead of returning undefined or waiting for a new response.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 keepPreviousData: true3});
Explanation: With this option, SWR will display the old data while it fetches the new one, instead of showing an empty loading state.
onLoadingSlow(key, config)
• How it works: This function is called when loading takes too long, defined by the loadingTimeout option.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 loadingTimeout: 5000,3 onLoadingSlow: (key, config) => {4 console.log(`The request for ${key} is taking too long!`);5 }6});
Explanation: Here, if the query exceeds 5 seconds, a message is displayed in the console indicating that the query is taking too long.
onSuccess(data, key, config)
• How it works: A callback that is executed when the query succeeds. You can use it for additional actions after the data is retrieved.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 onSuccess: (data, key, config) => {3 console.log('Data loaded successfully!', data);4 }5});
Explanation: When the data is successfully retrieved, this function is called and displays a message in the console with the data.
onError(err, key, config)
• Operation: Callback that is triggered in case of an error while retrieving the data.
Example:
1const { data, error } = useSWR('/api/posts', fetcher, {2 onError: (err, key, config) => {3 console.error(`Error loading ${key}:`, err);4 }5});
Explanation: If an error occurs, this function is called to display an error message in the console with the error details.
onErrorRetry(err, key, config, revalidate, revalidateOps)
• Operation: Allows you to customize the handling of errors and retry attempts.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 onErrorRetry: (err, key, config, revalidate, revalidateOps) => {3 if (err.status === 404) return; // Ne pas réessayer si l'erreur est un 4044 setTimeout(() => revalidate(revalidateOps), 5000); // Réessayer après 5 secondes5 }6});7
Explanation: Here, SWR does not retry in case of 404 error, but for any other error, it waits 5 seconds before retrying.
onDiscarded(key)
• How it works: This function is called when a request is ignored due to a race condition.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 onDiscarded: (key) => {3 console.log(`The request for ${key} was discarded.`);4 }5});
Explanation: If a query is canceled due to a race condition (e.g. a new query was launched before the first one completed), this callback will be called.
compare(a, b)
• How it works: A function that compares new data with old data to avoid unnecessary re-rendering.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 compare: (a, b) => JSON.stringify(a) === JSON.stringify(b)3});4
Explanation: Here, SWR compares the old and new data with a JSON.stringify() comparison. If they are the same, SWR does not trigger a re-render of the component.
isPaused()
• How it works: Function that allows you to pause revalidations. If it returns true, SWR ignores the retrieved data and errors until it returns false.
Example:
1const { data } = useSWR('/api/posts', fetcher, {2 isPaused: () => {3 const networkStatus = navigator.onLine;4 return !networkStatus; // Pauser la revalidation si l'utilisateur est hors ligne5 }6});7
Explanation: Here, if the user is offline (navigator.onLine is false), revalidations are suspended. Data and errors are not processed until the user comes back online.
Note: If the resource is immutable, you can disable all kinds of automatic revalidations for it. To do so, you can manually disable the above options to do so by setting them to false or use the useSWRImmutable hook.
Exemple:
1useSWR(key, fetcher, {2revalidateIfStale: false,3revalidateOnFocus: false,4revalidateOnReconnect: false5})*67// équivalent à8useSWRImmutable(key, fetcher)
Manually updating the cache with mutate
One of the most powerful features of SWR is the ability to manually update the cache via the mutate function. This allows you to locally modify the data without triggering a new request to the API.
Use case: Adding a new item
Let’s say you have a list of courses and you want to add a new course to the list without making a full API request again.
Here’s how to use mutate :
1const { data, mutate } = useSWR('/api/trainings', fetcher);23const addNewTraining = async (newTraining) => {4 // Envoyer la nouvelle formation à l'API5 const response = await api_admin.post('/trainings', newTraining);67 // Mettre à jour le cache localement sans revalider8 mutate((trainings) => [...trainings, response.data], false);9};
Explanation:
mutate((trainings) => […trainings, response.data], false): This line takes the current trainings from the cache (trainings), adds the new training (response.data), and updates the cache without performing any revalidation (the second argument false prevents the API request). Context: Creating an SWR hook in a separate file
Let's say you have a hooks.js file in which you create a hook to retrieve data that you will use in several components of your application.
Example: Hook to retrieve trainings
In your hooks.js file, you can create a custom hook to retrieve trainings from an API:
1// hooks.js2import useSWR from 'swr';3import axios from 'axios';4import { fetchWithCredentials } from '../utils/fetcher';5import { objectToQueryString, cleanObject } from '../utils/objectToQueryString';67// Créer une instance Axios pour l'API admin8export const api_admin = axios.create({9 baseURL: process.env.NEXT_PUBLIC_API_URL_ADMIN,10});1112// Hook personnalisé pour récupérer les formations admin13export const useAllTrainingsAdmin = (params) => {14 let query = objectToQueryString(cleanObject(params));1516 // Utilisation de SWR pour la requête17 const { data, error, isLoading, mutate } = useSWR(18 `${process.env.NEXT_PUBLIC_API_URL_ADMIN}/training/?${query}`,19 fetchWithCredentials20 );2122 return {23 trainings: data,24 isLoadingTraining: isLoading,25 mutateAllTraining: mutate, // Exposer la fonction mutate pour une utilisation future26 error,27 };28};
Usage in a component
Now you use this useAllTrainingsAdmin hook in a component to retrieve and display the trainings.
1// TrainingList.js2import { useAllTrainingsAdmin } from '../hooks';34const TrainingList = () => {5 // Appel du hook6 const { trainings, isLoadingTraining, mutateAllTraining } = useAllTrainingsAdmin({ page: 1 });78 if (isLoadingTraining) return <div>Chargement...</div>;910 return (11 <div>12 {trainings && trainings.map(training => (13 <div key={training.id}>{training.title}</div>14 ))}15 <button16 onClick={() => {17 const newTraining = { id: '123', title: 'Nouvelle Formation' };1819 // Mettre à jour manuellement le cache en ajoutant la nouvelle formation20 mutateAllTraining((trainings) => [...trainings, newTraining], false);21 }}22 >23 Ajouter une Formation24 </button>25 </div>26 );27};2829export default TrainingList;
Explanation of manual cache update with mutate
Step 1: Expose the mutate function
In the hooks.js file, when you create the custom hook with useSWR, you expose the mutate function by returning it under a name, for example mutateAllTraining :
1const { data, error, isLoading, mutate } = useSWR(...);2return {3 trainings: data,4 isLoadingTraining: isLoading,5 mutateAllTraining: mutate, // On expose mutate ici6 error,7};
This allows you to use mutateAllTraining in any component that calls this hook.
Step 2: Using mutate in a component
In the TrainingList component, you have a button to add a training, and when the user clicks on it, you update the SWR cache without making a new API request.
The key line is this:
1mutateAllTraining((trainings) => [...trainings, newTraining], false);
• mutateAllTraining : This is the mutate function you exposed in the hook.
• (trainings) => […trainings, newTraining] : Here, you pass a function that manually updates the cache by taking the current data (trainings) and adding the new training (newTraining). This updates the cache directly with this new data.
• false : This last argument tells SWR not to revalidate the data after the manual update. This means that you update the cache only, without triggering a new API request.
Why use mutate?
The advantage of mutate is that it allows you to update the cache optimistically. This means that you can instantly display new data to the user (such as adding a new training), without waiting for an API request. This improves the user experience by making the interface more responsive.
Using the useSWRMutation hook
useSWRMutation is a hook provided by SWR that allows you to perform mutations (such as POST, PUT, PATCH or DELETE requests) manually. Unlike useSWR which is designed for automatic GET requests, useSWRMutation allows you to trigger asynchronous requests on demand, without the state of other SWR hooks being automatically updated. This allows for more granular control when performing data modification operations on an API.
Basic structure of useSWRMutation
1const { trigger, data, error, isMutating } = useSWRMutation('/api/resource', mutationFunction, options);
trigger : A function to call to trigger the mutation.
data : The data returned after the mutation.
error : Any errors resulting from the mutation.
isMutating : A boolean indicating whether the mutation is in progress.
Basic example
If you have an API endpoint /api/posts to create a new post, you can configure useSWRMutation to send the data to the API.
1const createPost = async (url, { arg }) => {2 return axios.post(url, arg).then((res) => res.data);3};45export const useCreatePostMutation = () => {6 return useSWRMutation('/api/posts', createPost);7};
In your component you can trigger the mutation with trigger:
1const { trigger, isMutating } = useCreatePostMutation();23const handleSubmit = async () => {4 const newPost = { title: "New Post", body: "This is a new post" };5 await trigger(newPost);6};
Additional options in useSWRMutation
useSWRMutation also accepts additional options for more fine-grained mutation management, such as optimistic updating, cache management, etc.
optimisticData
This allows to update the data locally even before the mutation is confirmed, giving the illusion of an immediate response.
1const { trigger } = useSWRMutation('/api/posts', createPost, {2 optimisticData: (posts = []) => [...posts, { title: "Temp Post", body: "This is a temporary post" }]3});
populateCache
Use this option to update the SWR cache manually after mutation. This is useful when you want to synchronize the server response with other components that use the same cache key.
1const { trigger } = useSWRMutation('/api/posts', createPost, {2 revalidate: false,3 populateCache: (result, datas) => {4 if (datas) {5 return [result, ...datas]6 } else {7 return [result]8 }9 },10});
onSuccess and onError
You can perform additional actions after the mutation succeeds or fails.
1const { trigger } = useSWRMutation('/api/posts', createPost, {2 onSuccess: (data) => console.log("Post created!", data),3 onError: (error) => console.error("Failed to create post", error)4});
Full example with an update
1export const usePatchMutation = (id: string) => {23 // A importer lorsqu'on souhaite utiliser la 2eme méthode4 const { mutateAllPosts } = useAllPosts()56 useSWRMutation(7 `https://jsonplaceholder.typicode.com/posts/${id}`,8 updatePost,9 {10 revalidate: false,11 populateCache: (result, datas) => {12 if (datas) {13 return [result, ...datas]14 } else {15 return [result.data]16 }17 },18 onSuccess: (result) => {19 // Ici on met à jour manuellement20 // la donnée modifié dans la clé qui stok l'ensemble des post21 mutate(22 'https://jsonplaceholder.typicode.com/posts',23 (posts: any[]) => posts?.map((post) => {24 return post.id === result.id ? result : post25 }),26 {27 revalidate: false28 }29 )3031 // On peut egalement le faire de cette façon32 // mutateAllPosts correspond au mutate deja rattaché a la clé qui stok l'ensemble des post33 mutateAllPosts(34 (posts: any[]) => posts?.map((post) => {35 return post.id === result.id ? result : post36 }),37 {38 revalidate: false39 }40 )41 },42 }
In your component, you can trigger the mutation with trigger:
1const params = useParams<{id: string}>()2const { trigger, isMutating } = usePatchMutation(params.id)34const sendForm = async (dataToSend: any) => {5 try {6 await trigger(dataToSend)7 } catch (error) {8 console.error('Erreur lors de la création du post', error);9 }10}
KAANARI IN YOUR INBOX
Kaanari presents stories of creators that inspire, inform, and entertain.
Subscrire to our newsletter so you never miss a story.