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.
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 true
onSuccess(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 useSWR
options
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.
useSWR
SWR 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.
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 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
.
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:
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 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.
Hey there, want to help make our blog better?
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 nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]