DEV Community

Cover image for Four Ways to Opt Out of Static Rendering in Next.js
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on

Four Ways to Opt Out of Static Rendering in Next.js

by Temitope Oyedele

Next.js

tries to statically prerender or generate as many pages as possible by default to optimize our application. However, sometimes, we don't want this behavior and want to opt out of it. This article looks at four ways to opt out of static rendering in Next.js.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.

OpenReplay

Happy debugging! Try using OpenReplay today.


Why opt out of static rendering? There are several reasons for this. To get a better understanding, we’ll be classifying them into three, namely:

  • Limitations of static rendering for certain types of content.

  • Performance considerations for dynamic content.

  • User experience implications.

Limitations of static rendering for certain types of content

While static rendering provides excellent performance benefits, it has limitations for certain types of content, which are:

  • Real-time data requirements: Static rendering is not suitable for applications requiring real-time data updates, such as live sports scores, stock market information, or social media feeds. These types of content need to be fetched and displayed dynamically to ensure users receive the most current information.

  • Personalized content: Personalized content, which varies based on user preferences, behavior, or location, cannot be effectively delivered through static rendering. Dynamic rendering allows the application to tailor content to individual users, enhancing their experience.

  • Frequently updated information: Websites that frequently update their content, such as news sites or blogs, need a more flexible rendering approach. Static generation at build time would require frequent rebuilds and deployments, which is impractical for such scenarios.

Performance considerations for dynamic content

Dynamic content can be optimized for performance using various techniques in Next.js, ensuring that users receive fast and responsive interactions even when static rendering is not an option.

User experience implications

Opting out of static rendering can significantly improve user experience by providing up-to-date, relevant, and personalized content. This responsiveness is crucial for maintaining user engagement and satisfaction.

Method 1: using fetch options - cache or revalidate

Next.js provides fetch options that allow developers to control how data is cached and revalidated. These options include the cache and revalidate, which control how data is stored and updated.

The cache option specifies how long the fetched data should be stored before it is considered stale. It tells Next.js how to handle fetch request caching and determines whether fresh data is fetched from the source or if cached data can be used. It can also influence load times and server load by controlling how often new requests are made.

The revalidate allows you to update static content after a specified time. It's part of the incremental static regeneration (ISR) strategy, which combines the benefits of static generation with the ability to update content dynamically.

How to implement the cache option

To implement the cache option, you need to add the syntax to your fetch request:

const response = await fetch(url, { cache: 'option' });
Enter fullscreen mode Exit fullscreen mode

Replace option keyword with one of the following cache strategies:

  • no-store: Always fetch fresh data, bypassing the cache entirely.

  • no-cache: Validate the cached data with the server before using it.

  • force-cache: Use cached data if available (this is the default behavior).

  • reload: Always fetch fresh data and update the cache.

Let's examine an example of how to use the cache option in a simple time update scenario.

Create an API route to serve the current time.

Inside the app folder, create another folder called api. Inside the api folder, create a file called route.js. Add the following code:

export async function GET(request) {
  const currentTime = new Date().toLocaleString();
  return new Response(JSON.stringify({ time: currentTime }), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
 },
 });
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we define an asynchronous GET function to process HTTP GET requests. When called, the function returns the current date and time by constructing a new Date object and converting it to a locale-specific string using the .toLocaleString() method.

Next, replace the code in your page.js with the following code:

import React from "react";

async function getTime() {
  const res = await fetch("http://localhost:3000/api", {
    cache: "no-store",
 });

  if (!res.ok) {
    throw new Error(`Network response was not ok: ${res.statusText}`);
 }

  const json = await res.json();
  return json.time;
}

export default async function Home() {
  const now = await getTime();

  return (
    <section className="py-24">
      <div className="container font-bold">
        <h1 className="text-3xl">Dynamic Rendering</h1>
        <h2 className="mt-6 text-xl text-emerald-600">{now}</h2>
      </div>
    </section>
 );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we define an asynchronous function, getTime(), that fetches the current time from the local API endpoint we created. The { cache: 'no-store' } ensures you always fetch the current time from the server, bypassing any cached versions. This is useful for highly dynamic content that must be updated on every request.

Also, with the { cache: 'no-store' }, rendering is performed on the server side, providing a dynamic response each time the page is loaded.

How to implement the revalidate option

To implement the revalidate option, pass it inside the next object in your fetch options.

fetch(url, {
  next: { revalidate: 3 }, 
});
Enter fullscreen mode Exit fullscreen mode

Still using the clock example, replace the code inside the page.js with this:

import React from "react";

async function getTime() {
  const res = await fetch("http://localhost:3000/api", {
    next: { revalidate: 3 },
 });

  if (!res.ok) {
    throw new Error(`Network response was not ok: ${res.statusText}`);
 }

  const json = await res.json();
  return json.time;
}

export default async function Home() {
  const now = await getTime();

  return (
    <section className="py-24">
      <div className="container font-bold">
        <h1 className="text-3xl">Dynamic Rendering</h1>
        <h2 className="mt-6 text-xl text-emerald-600">{now}</h2>
      </div>
    </section>
 );
}
Enter fullscreen mode Exit fullscreen mode

The code above is similar to our previous example but uses the revalidate option instead. It defines the async function getTime, which retrieves the current time from the API endpoint.

The { revalidate: 3 } ensures that the data will be revalidated every 3 seconds. This means that the page will be statically generated, but Next.js will check for updates to the data every 3 seconds. If there are changes, the page will be refreshed with fresh data. This approach provides a good balance between performance and data freshness.

Method 2: route config option - force-dynamic

Imagine you were using a database query, and you’re using a database client to create a connection to a database or query a database, and we are not using the fetch in that instance to be able to pass the cache options to it. This is where the route config comes into place.

Route config allows you to define how different routes should be rendered. With route config, you can determine which routes should be statically generated and which should be dynamically rendered.

How to implement the route config option

To implement the route config option, you export a dynamic segment from your page:

export const dynamic = 'force-dynamic'
Enter fullscreen mode Exit fullscreen mode

This will tell Next.js to always render the page dynamically.

Let's look at an example of how to use the route config option. We'll use the force-dynamic example to simulate a simple stock price tracker where data changes frequently and needs to be current for each page view.

export const dynamic = "force-dynamic";

export default function Page() {
  const currentTime = new Date().toLocaleString();
  const randomNumber = Math.floor(Math.random() * 100) + 1;

  return (
    <div>
      <h1>Dynamic Stock Price Simulator</h1>
      <p>Current server time: {currentTime}</p>
      <p>stock price: ${randomNumber.toFixed(2)}</p>
      <p>This page is dynamically rendered on each request.</p>
    </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we simulate a dynamic stock price tracker by generating a fresh current time and a random stock price for each request. You'll notice we export dynamic = 'force-dynamic' at the top of the file. This is to ensure the page is always dynamically rendered.

Without force-dynamic, Next.js might optimize by caching the rendered output or only re-rendering periodically. With this setting, you're guaranteed a fresh render on every request, which is useful for content that must always reflect the latest data or request-specific information.

Method 3: dynamic function

Another way to opt out of static rendering or make a page dynamic is to use the dynamic functions inside the page components. Dynamic functions in Next.js are server-side functions that run at request time, allowing for dynamic content generation.

Some key dynamic functions include:

How to implement dynamic functions

To implement dynamic functions, you’ll need to import and call them into your component or page:

import { cookies, headers } from "next/headers";
export default function Page() {
  const cookieStore = cookies();
  const headersList = headers();
}
Enter fullscreen mode Exit fullscreen mode

Below is an example of a page that displays some information from the request headers():

import { headers } from "next/headers";

export default function Page() {
  const headersList = headers();

  const userAgent = headersList.get("user-agent") || "Unknown";
  const referer = headersList.get("referer") || "Direct visit";
  const acceptLanguage = headersList.get("accept-language") || "Not specified";

  return (
    <div>
      <h1>Request Headers Information</h1>
      <ul>
        <li>
          <strong>User Agent:</strong> {userAgent}
        </li>
        <li>
          <strong>Referer:</strong> {referer}
        </li>
        <li>
          <strong>Accept-Language:</strong> {acceptLanguage}
        </li>
      </ul>
    </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we retrieve and display information from the request headers using the headers() function. This function allows us to access various HTTP headers sent by the client. These values are then displayed within the component.

The mere presence of headers() in this component automatically opts the entire route into dynamic rendering. Without dynamic rendering, the component couldn't show unique, request-specific header information for each visitor.

Dynamic functions are ideal for pages that require real-time data or user-specific information, such as dashboards or personalized profiles.

Method 4: unstable_nostore

While the other ways discussed affect the page layout, the unstable_nostore is quite different as it gives you more granular control over opting out of caching in even specific components.

According to the documentation, the unstable_nostore can be used to explicitly prevent static rendering and assign which components should not be stored.

The unstable_noStore can be used to declaratively opt out of static rendering and indicate a particular component should not be cached.

It is designed to bypass caching mechanisms for specific data fetches or server actions. It ensures that data is always retrieved fresh from the source.

How to implement the unstable_nostore function

To implement the unstable_nostore function, you’ll have to first import the function and then call it at the beginning of your data-fetching logic:

import { unstable_noStore as noStore } from "next/cache";
function fetchData() {
  noStore();
}
Enter fullscreen mode Exit fullscreen mode

Below is an example using it got time update:

import { unstable_noStore as noStore } from "next/cache";

async function getCurrentTime() {
  noStore();
  return new Date().toLocaleTimeString();
}

export default async function Page() {
  const time = await getCurrentTime();

  return (
    <div>
      <h1>Dynamic Time Page</h1>
      <p>The current server time is: {time}</p>
      <p>This page is dynamically rendered on each request.</p>
    </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we created a dynamic time page that displays the current server time. The noStore() function is called within the getCurrentTime function to ensure that the result is not cached and that the function retrieves fresh data on every request. This forces the page to be dynamically rendered each time it is requested, as no cached version of the content is stored or reused.

This approach provides more granular control over dynamic rendering compared to the dynamic = 'force-dynamic' config or using dynamic functions like headers() or cookies(). It allows you to specify exactly which parts of your code should opt out of caching and trigger dynamic rendering.

That's how you opt out of static rendering in Next.js

Opting out of static rendering in Next.js allows you more freedom when dealing with dynamic material. You can use fetch options, route config, dynamic functions, and unstable_noStore functions to create responsive and up-to-date online apps. It is critical to strike a balance between the requirement for new content and performance considerations. This is to ensure a great user experience.

Top comments (0)