You need to enable JavaScript to run this app.

My Data-Fetching Preference in Next.js

Summary:

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.

Category
Next.js
Time to read
15 min.
Published at
2022-03
My Data-Fetching Preference in Next.js

Introduction

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

Data Fetching Strategies in Next.js Apps

1. Server Side Rendering ( SSR )

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 ).

  • Benefits:
    • A server-side rendered application enables pages to load faster, improving the user experience.
    • Rendered content at first ready for search engines crawler robots which is ideal for SEO.

  • Downsides:
    • Slower TTFB
    • The webpage interactions are fewer.
    • Slow page rendering.
    • Full UI reloads.
    • Frequent server requests.
    • A heavy load for server on more requests

2. Client side rendering

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.

  • Benefits:
    • Faster TTFB
    • Reducing the load from the server and transferring it to the client machine.

  • Downsides:
    • Slower FCP
    • Heavier initial page load(loading framework)
    • Empty content for search engines crawler robots ( almost no SEO )

3. Static Site Generation ( SSG )

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.

  • Benefits:
    • Faster load times
    • Rendered content at first, ready for search engines crawler robots to index which is ideal for SEO.

  • Downsides:
    • Builds will take longer to complete
    • No user-specific content
    • Once you build a page, there is no way to revalidate its data

4. Incremental Static Regeneration ( ISR )

ISR is a new paradigm introduced by Next.js. It combines the advantages of SSG and SSR to build websites.

  • Benefits:
    • All of SSG benefits
    • Fast TTFB ( as most of the times we serve statically generated page )
    • Almost Fresh content, as you can set the max stale time

  • Downsides:
    • Builds will take longer to complete
    • No user specific content
    • Slow TTFB for pages that wasn't statically cached before.

5. On-demand Revalidation

Since 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.


  • Benefits:
    • All of ISR benefits
    • Fresh content, as you can revalidate your caches at any time.

  • Downsides:
    • All of ISR Downsides
    • Not ideal for cases that you have so many pages that get revalidated frequently.

6. On-Demand Revalidation/ISR/SSG + CSR

This 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.

Important note

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.

Conclusion

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.

Favourite Books
BooksPoems
Favourite Songs
PlaylistsArtists
Favourite Shows
AnimationsSeriesAnime