In this blog post, we explore various data-fetching strategies available in Next.js applications. The blog discusses Server Side Rendering (SSR), Client Side Rendering (CSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), and On-Demand Revalidation, highlighting the benefits and downsides of each approach. The blog provides examples and explanations for each approach and advises developers to choose the most suitable strategy based on their specific project requirements, considering factors like SEO, user-specific data, and overall performance.
Front-end programming is getting more advanced every day, new solutions have entered the scene based on recent needs, and we have to adapt our apps with performant solutions.
When it comes to data fetching and server state management, there are so many different approaches to choose from. some of those are better than the others but in some cases, you will just end up picking the one that its developer experience suits you better. TBH most of the time you have to adapt yourself and your approaches to business requirements but there will be always some right ways to choose. Here I want to show you the way I personally like to implement my projects data-fetching and server state management.
But first, let's have a quick overview of different options of data fetching strategies in next.js apps. You can read more about those concepts here. you will need some basic React and Next.js understanding to understand these concepts
With SSR you can generate an entire page on demand for every user specifically and show a completely rendered page on the first load, pretty convenient huh? not at all. An important point here is that you are no longer limited to client-side coding anymore as you were in react development. The decisions you make will directly affect your server performance. You can use the SSR approach however you should strongly avoid using it until you have to do so( probably never ).
Client-side rendering is the way we coded in react projects. In this method, we fetch page needed data on the client side and the rendering process will take place in the client's machine.
We already know about statically generated websites. It's not a new concept, we were using it since the beginning. This method lets you build your pages at build time instead of doing it on demand for every single request. But building rich web experiences wasn't something we could do easily before, tools like Next.js, gatsby, nuxt, jekyll and ... introduced us better ways of building static-generated websites with more dynamic performance.
ISR
is a new paradigm introduced by Next.js. It combines the advantages of SSG
and SSR to build websites.
SSG
benefitsSince next.js
version 12, you can use the stable revalite
method and bring the power of webhooks at your service.
You first need to know what on-demand revalidation is.
Gladly, I don't have to reinvent the wheel in this scenario and you can read
the comprehensive documentation of next.js about that.
Using it is pretty straighforward but I will have an example here for you to show how I like to implement it.
In the below example I am using ISR
and also built a revalidation api
and gave it to backend folks and headless CMS to call as a webhook on data change:
export const getStaticPaths: GetStaticPaths = async function ({ locales }) {
const products = await ProductServices.listMostViewedProducts();
const paths = (locales || []).reduce(
(acc, locale) => [
...acc,
...(products?.map(({ slug }) => ({
params: { slug },
locale,
})) || []),
],
[] as GetStaticPathsResult["paths"]
);
return { paths, fallback: "blocking" };
};
type PageProps = { product: IProduct };
export const getStaticProps: GetStaticProps<Dictionary, ProductPageProps> =
async function ({ locale, params }) {
try {
const product = await ProductServices.get(params.slug);
if (!product) return { notFound: true };
return {
props: { product },
revalidate: 604800,
};
} catch (e) {
if (e.statusCode === HTTPStatusCodes.NotFound) return { notFound: true };
throw e;
}
};
The above code is for generating some product pages at build time and adding a week stale time to its cache using ISR. But what if the data of a product changed a second after the cache was generated? The people who changed the data obviously don't want to wait for a week to see the changes, So we need some kind of webhook to be called whenever we want to revalidate our caches.
The below code is a next.js serverless api that demonstrates that webhook:
import { HTTPStatusCodes } from "@constants/http";
import type { NextApiRequest, NextApiResponse } from "next";
/** Revalidates cache of an statically generated page */
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<{ revalidated?: boolean; message?: string }>
) {
const { method } = req;
if (method !== "PUT") {
res.setHeader("Allow", ["PUT"]);
return res.status(405).end(`Method ${method} Not Allowed`);
}
try {
const AUTHORIZATION_HEADER_TYPE = "RevalidationSecret";
const revalidationSecret = process.env.ON_DEMAND_REVALIDATION_SECRET;
const providedRevalidationSecret = req.headers.authorization?.replace?.(
`${AUTHORIZATION_HEADER_TYPE} `,
""
);
const pathToRevalidate: string = req.body?.path;
if (providedRevalidationSecret !== revalidationSecret) {
return res
.status(HTTPStatusCodes.Unauthorized)
.json({ message: "INVALID REVALIDATION SECRET" });
}
if (!pathToRevalidate) {
return res
.status(HTTPStatusCodes.BadRequest)
.json({ message: "BAD REQUEST" });
}
await res.revalidate(pathToRevalidate);
console.log(
`%cpath: \`${pathToRevalidate}\` revalidated successfully.`,
"color: green"
);
return res.status(HTTPStatusCodes.Ok).json({ revalidated: true });
} catch (error) {
return res
.status(HTTPStatusCodes.InternalServerError)
.json({ message: "ERROR OCCURRED WHILE REVALIDATING" });
}
}
the ON_DEMAND_REVALIDATION_SECRET
environment variable is a secret
shared between frontend and the headless CMS to protect your caches
to get revalidated frequently by malicious users.
You can add host value check or a more sofisticated
secret generator to make your webhook safer.
ISR
benefitsISR
DownsidesThis is my favorite one, you can use On-Demand Revalidation, ISR,
or SSG
for building most of the important parts of a page at build time whilst
you can get more fresh and user-specific data on the client side.
For this approach, we need to separate our data into two different parts:
shared between request and specific for every request.
in the example below you can see a payload of data for a product page of an e-commerce website.
interface IProduct {
id: number;
name: string;
description: string;
isLiked: Boolean;
similars: IProduct[];
}
id
is something that will not change, name
and description
may get change sometimes,
isLiked
and similars
are some parts of this response that will be different based on request.
The best way to implement this page is to first fetch data at build time with help of ISR, removing its dynamic parts( isLiked, similars ), and passing remains to the page as props.
export const getStaticProps = async ({ params }) => {
const result = await ProductServices.getDetail(params.slug);
return {
props: { product: result.product },
};
};
}
export const getStaticPaths = async ({ locales }) => {
const priorProducts = await ProductServices.getPriors(1000);
const paths = priorProducts.reduce((acc, { slug }) => {
locales.forEach((locale) => {
acc.push({ params: { slug }, locale });
});
return acc;
}, []);
return {
paths,
fallback: false, *
};
}
* You can do some interesting stuff with fallback key, you can read more about it here.
On the client side, I use SWR as a server state management tool, you can use react-query or any other tool that u like.
// hooks/useProduct.js
import useSWRImmutable from "swr/immutable";
import { useDebugValue } from "react";
import endpoints from "@constants/endpoints";
import Http from "@utils/Http";
const swrFetcher = async (url) => {
try {
const response = await Http.request({ url });
return response;
} catch (err) {
return err;
}
};
export default function useProduct(slug) {
const key = endpoints.product + "?slug=" + slug;
const swr = useSWRImmutable(key, swrFetcher);
useDebugValue(swr.data);
return swr;
}
// pages/product/[slug].js
const router = useRouter();
const product = useProduct(router.query.slug, { initialData: props.product });
With this we can use product.data
everywhere we want. since we filled it at first render,
some parts of the product state aren't completed yet( dynamic parts ),
but after all the things we did with ISR/SSG, now we have most of our data
so we can statically generate parts that are needed for crawler robots
to index our page and for end-users to interact with it.
To complete our amazing user experience,
for other parts of our page we can use skeleton loading like this:
return (
<>
<Head title={product.name} description={product.description} />
<ProductData {...product.data} />
<SimilarProducts loading={!product.data?.similars} list={product.data?.similars} />
<>
)
Inside SimilarProducts
component, we show a skeleton loading
representing future UI as long as the loading prop is true.
In this method, we have all the benefits of ISR
and SSG
without making the user wait for data that has no use for us on the first render.
To prevent a bugging flash on screen after loading data
and also to make a better user experience we use skeleton loading,
you can paint your own loading with SVG or you can use third-party packages
like react-content-loader.
Since on the client-side, we are calling the same API we called at build time, we can lighten API response by using GraphQL to just fetch parts of data that we want.
As we discussed, choosing SSR blindly is no longer an straighforward option for web applications and alternative strategies with fixing issues and expanding the limitations, has became more optimal ways for implementing web applications All the things said, now you can choose a better way to implement your pages based on your need.
For pages that SEO does not matters, like a user profile page,
you need to use client-side rendering( first strategy ) without hesitation,
and since you are not using any SSR data fetching method here,
next.js
will statically generate some parts of this page like Layout, Head and ... for you.
If you have a shared API for all your pages e.g. list of all countries that you need everywhere in your app, you can mix it with the 5th strategy and fetch it at build time or you can use middlewares.
In cases that SEO does matters for your page, you need to first distinguish different parts of your data.
If needed data to render your page, is completely shared between different clients, you can choose between 3rd or 4th strategies based on the required time for revalidation of your data.
But if your data has dynamic parts that have different values for different users
( like isLiked
and similars
properties in our example ),
you should choose the 5th strategy for your page.