useSWR
Editor’s note: This article, updated by Elijah Agbonze on 28 March 2024, dives into the advanced use cases of the useSWR Hook for Next.js development. It covers pagination, conditional rendering, caching, using useSWR for subscriptions, conditional fetching, and more.
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 look at some important features and support offered by SWR.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
useSWR Hookconst { data, isLoading, error } = useSWR(key, fetcher, options);
useSWR paramsThe 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 key name is because it actually is a key. It is a unique string and in some cases, can be a function, object, or 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}`);
return res.json()
};
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}`);
return res.json()
};
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 when 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 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. The default is trueonSuccess(data, key, config): Callback function when a request is successful. If the request returns an error, the onError callback function can be usedshouldRetryOnError: Turns on or off retrying when an error occurs. The default is true. If set to true, you can also determine the max retry count with the errorRetryCount option and the retry interval with the errorRetryInterval optionHere is the full list of useSWRoptions parameters.
useSWR return valuesThe 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 returns undefined if not loadederror: Any error thrown in the fetcher function. It could be a syntax, reference, or even an error from the API. We will take a look at how we can throw errors later in this articleisLoading: Returns true/false if there’s an ongoing request and no loaded dataisValidating: Returns true/false if there’s an ongoing request or revalidation is loading. It is almost similar to isLoading. The only difference is that isValidating also triggers previously loaded datamutate: A function for making changes to the cached data specific to this cached key onlyLet’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 into 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 that we turned off revalidation. This is because we’re fetching immutable data, so we can turn off revalidation to prevent unnecessary request calls.
useSWR use casesIn this section, we will explore some advanced use cases of useSWR that will be useful when building your app. We will look at pagination, caching, and conditional rendering.
useSWRSWR comes with built-in support for easily handling pagination. There are two common pagination UIs: 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 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 provides a different hook for handling infinite loading, useSWRInfinite:
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 setSize function 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,
});
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.
localStorage for persisting dataSWR 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") || "[]"));
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.
useSWRThe 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 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.
useSWR for subscriptionOne other useful feature of useSWR 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 app
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;
When fetching similar data across your application, it is best to make fetching the data reusable to avoid duplicate code and optimize 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 a declarative way, which means you only need to specify what data is used by the component.
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 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 useSWR, { 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 useSWR, { 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>
);
};
Calling mutate with key will trigger a revalidation for all useSWR hooks that use 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 might sound 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 automatically set to false.
useSWRAs 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:
One benefit of using useSWR is that it returns errors specific to a component, and you can display what went wrong right there. We’ve seen examples 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.
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 GraphQLFetching 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;
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.
useSWR performance optimizationuseSWR 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 availableuseSWR mutate function can be used for data prefetching. The mutate 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 cacheuseSWR logic in custom hooks to promote reusable data fetching and make it easier to manage data fetching across your applicationuseSWR makes it easier to fetch only the data that the current component requires, rather than fetching all data simultaneously. This helps minimize unnecessary network callsTesting useSWR can seem daunting at first, but it’s an essential step to ensure your React applications work smoothly. Some of these strategies include:
mutate function from useSWR. Verify that the data is indeed prefetched and readily available in the cache when neededHaving 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 an 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 add revalidate to the returned value from getStaticProps:
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.
Assuming you’ve installed Next.js for all its other benefits, like routing, and you don’t want the option of rendering your pages from the server, Nextjs provides a way to disable SSR for your pages and we will look at it now. Note that this will affect your SEO performance.
By default, Nextjs provides an option to disable SSR when lazy loading your component:
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })
Now you’d have to specify the ssr option for each page where you intend to disable the SSR. An easier, reusable way would be to create a reusable component that does this by default:
import dynamic from "next/dynamic";
import React from "react";
const NoSSRWrapper = ({ children }) => (
<React.Fragment>{children}</React.Fragment>
);
export default dynamic(() => Promise.resolve(NoSSRWrapper), {
ssr: false,
});
Now, where you intend to use it, import the NoSSRWrapper component and wrap it around your component like so:
import NoSSRWrapper from "../no-ssr";
const Posts = () => {
return (
<NoSSRWrapper>
<h2>Hello World</h2>
</NoSSRWrapper>
);
};
export default Posts;
So far, we’ve seen fetching data on both the client-side and server-side and this brings the question of which should you use? Or which is better?
It’s important to recognize that client-side and server-side functioning each serves distinct purposes. The key is to determine which method aligns best with your specific goals for a web page.
Client-side data fetching fetches your data after the page has been rendered. It is useful if:
When you consider these, you’ll find out that the common use case for client-side data fetching is on users’ dashboards.
Server-side data fetching fetches your data before the page is rendered. With the Nextjs 13.4 stable release of the App Router, you don’t have to use getServerSideProps or getStaticProps to fetch data from the server. You can simply fetch data from anywhere on the page.
Pre-rendering your data is always the best solution for SEO performance because it allows your page to be indexed. The beautiful thing about client-side and server-side fetching in Next.js is the fact that they can coexist in a project, and not just that — they can coexist on a page, as well. So for pages where you’d want data to be indexed by search engines like blog pages, etc., consider fetching the data on the server side.
In this guide, we’ve seen how easy it is to fetch, prefetch, cache, mutate, and revalidate data regardless of the size using useSWR. We also looked at how to fetch from two different kinds of API (REST and GraphQL) using custom fetcher functions.
Each feature that we discussed is important and can be useful in your day-to-day client-side or server-side data fetching. There are many other features you can check out in the official useSWR docs. Happy hacking!
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 captures console logs, errors, network requests, and pixel-perfect DOM recordings from user sessions and lets you replay them as users saw it, eliminating guesswork around why bugs happen — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
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.

Test out Meta’s AI model, Llama, on a real CRUD frontend projects, compare it with competing models, and walk through the setup process.

Rosario De Chiara discusses why small language models (SLMs) may outperform giants in specific real-world AI systems.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.
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 now