#ISR next14 app router. How to fetch data once and use it on different pages?

1 messages · Page 1 of 1 (latest)

feral kernel
#
// app/page.tsx (index page):
export default async function Page() {
  const res = await fetch("http://127.0.0.1:3000/api/products", {
    next: { revalidate: 10 },
  });
  const data = await res.json();

  return (
    <div>
      <h1>Home page</h1>
      {data?.id}
    </div>
  );
}

// app/catalog/page.tsx (catalog page):
export default async function Page() {
  const res = await fetch("http://127.0.0.1:3000/api/products", {
    next: { revalidate: 10 },
  });
  const data = await res.json();

  return (
    <div>
      <h1>Catalog page</h1>
      {data?.id}
    </div>
  );
}

I am using Next.js v14 app router. I need to fetch data from my CMS API (http://127.0.0.1:3000/api/products) and use the data on both the index and catalog pages.

I have read docs in docs that if I need to use the same data in multiple components, I do not have to fetch data globally, nor forward props between components. Instead, I can use fetch or React cache in the component that needs the data without worrying about the performance implications of making multiple requests for the same data. This is possible because fetch requests are automatically memoized.

However, when I build my application, I notice that two fetch requests have been made, one during the index page building process and another during the catalog page building process (I guess). I would like my app to make a single fetch request to my CMS API and use the received data on several different pages.
How to achieve this?

And yes I want to keep using app router so things like getStaticProps are not relevant I guess.

fluid mapleBOT
#

Hi @feral kernel. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

In Next.js, the fetch function is automatically memoized, which means that if you call a fetch function for the same data in multiple places in a React component tree, it will only be executed once. However, this memoization only applies to the React Component tree and does not apply to Route Handlers as they are not a part of the React component tree. Also, the memoization only lasts the lifetime of a request, and once the route has been rendered and the rendering pass is complete, memory is "reset" and all request memoization entries are cleared (source (https://nextjs.org/docs/app/building-your-application/caching#request-memoization)).

In your case, you are making fetch requests in two different pages (index and catalog). These are separate routes and are not part of the same React component tree, so the fetch requests are not memoized across these pages. Each page is making its own fetch request when it is being built.

If you want to fetch data once and use it across multiple pages, you might need to reconsider your data fetching strategy. One possible solution could be to fetch the data in a parent component (like a custom _app.js file) and then pass the data down to the child components (your pages) via props or context. However, this approach might not be suitable for your use case if the data needs to be revalidated frequently.

Another approach could be to use a state management library like Redux or Zustand to store the fetched data and make it accessible across your application. However, this might introduce additional complexity to your application and might not be necessary if you only need to share data between a few pages.

#

In conclusion, while Next.js does provide automatic memoization for fetch requests, this feature might not be sufficient for your use case. You might need to consider other data fetching and state management strategies to share data across multiple pages.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@feral kernel kapa.ai is still learning and improving, please let us know how it did by reacting below

feral kernel
#

@fluid maple Lets say I have 10 pages that need the same data I get from a single CMS API call. So each time I build my app 10 absolutely identical fetches will be made, stressing the CMS for no actual reason. I find it hard to believe that there is no elegant solution to avoid this redundancy.

You suggested me to use state managers. However, I'm not convinced this approach will solve the issue because the fetches are made in parallel as the pages are being built. As a result, each page still triggers its own fetch call.

fluid mapleBOT
#

Let me check the knowledge base for you. One moment! :books:

#

You're correct that in the current setup, each page will trigger its own fetch call during the build process. This is because each page in Next.js is built independently and doesn't share state or memoization with other pages.

However, there's a way to fetch the data once and use it across multiple pages using Next.js features. You can create a shared function in a lib/ directory that fetches the data from your CMS API. This function can then be imported and used in the getStaticProps function of each page. Here's an example:

// lib/load-data.js
export async function loadData() {
  const res = await fetch("<http://127.0.0.1:3000/api/products",> {
    next: { revalidate: 10 },
  });
  const data = await res.json();
  return data;
}

// pages/index.js
import { loadData } from '../lib/load-data'
export async function getStaticProps() {
  const data = await loadData();
  return { props: { data } }
}

// pages/catalog.js
import { loadData } from '../lib/load-data'
export async function getStaticProps() {
  const data = await loadData();
  return { props: { data } }
}

In this setup, the loadData function is shared between the getStaticProps functions of the index and catalog pages. This means that the data fetching logic is centralized in one place, making it easier to manage and update if needed.

However, this approach still results in multiple fetch requests to your CMS API during the build process, one for each page that uses the loadData function. This is because getStaticProps runs at build time and is executed separately for each page.

Unfortunately, Next.js doesn't currently provide a built-in way to share fetched data across multiple pages at build time. This is a limitation of the static generation model, where each page is built independently and doesn't share state or memoization with other pages.

#

If making multiple fetch requests to your CMS API during the build process is a concern, you might need to consider other data fetching strategies or frameworks that better suit your needs. For example, you could use a server-side rendering model where the data is fetched once per request, or a client-side rendering model where the data is fetched once and then stored in a state management library or cache for reuse across multiple pages.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@feral kernel kapa.ai is still learning and improving, please let us know how it did by reacting below

feral kernel
#

@fluid maple However I have noticed that when the build process is done and the app is running (start) - only one fetch call is made for revalidation, and the received data is successfully cached and reused by other pages that require the same data from the same fetch call. This behavior is desirable and efficient unlike the behavior we see in the building phase.

However, I'm wondering if there is a way to avoid fetching anything during the build phase altogether?

Let's say my app has 10 pages that all need the same data from the same fetch call. If we can skip fetching for this data 10 times during the build phase, we can just start the app and have the revalidation trigger only one fetch call, which will be successfully cached.
This way, all pages can share the fetched data until the next revalidation, which will trigger a new fetch call and so on.

What are your thoughts on this?

fluid mapleBOT
#

Let me check the knowledge base for you. One moment! :books:

#

Yes, you can avoid fetching data during the build phase by using client-side data fetching instead of server-side data fetching. In this approach, you fetch the data on the client-side (in the browser) after the page has been rendered and then update the page with the fetched data.

Here's how you can do it:

// lib/load-data.js
export async function loadData() {{
  const res = await fetch("<http://127.0.0.1:3000/api/products",> {{
    next: {{ revalidate: 10 }},
  }});
  const data = await res.json();
  return data;
}}

// pages/index.js
import {{ useEffect, useState }} from 'react';
import {{ loadData }} from '../lib/load-data'

export default function Page() {{
  const [data, setData] = useState(null);

  useEffect(() => {{
    loadData().then(setData);
  }}, []);

  if (!data) return 'Loading...';

  return (
    <div>
      <h1>Home page</h1>
      {{data?.id}}
    </div>
  );
}}

// pages/catalog.js
import {{ useEffect, useState }} from 'react';
import {{ loadData }} from '../lib/load-data'

export default function Page() {{
  const [data, setData] = useState(null);

  useEffect(() => {{
    loadData().then(setData);
  }}, []);

  if (!data) return 'Loading...';

  return (
    <div>
      <h1>Catalog page</h1>
      {{data?.id}}
    </div>
  );
}}

In this setup, the loadData function is called in a useEffect hook after the page has been rendered. The fetched data is then stored in a state variable using the useState hook and the page is updated with the fetched data.

This approach avoids making fetch requests during the build phase and instead makes the fetch requests on the client-side after the page has been rendered. This means that the fetch requests are made only once per revalidation period, regardless of how many pages use the fetched data.

#

However, this approach has a trade-off: it can lead to a "Loading..." state or a flash of unstyled content (FOUC) as the page is initially rendered without the fetched data and then updated once the data has been fetched. You can mitigate this by using a loading spinner or a skeleton screen to indicate that data is being loaded.

Also, keep in mind that client-side data fetching does not benefit from Next.js's automatic static optimization and server-side rendering features. This means that the fetched data will not be included in the initial HTML sent to the browser, which can impact SEO and performance.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@feral kernel kapa.ai is still learning and improving, please let us know how it did by reacting below

feral kernel
#

@fluid maple Client-side data fetching is not what we want, at all, so your amazing concept is completely irrelevant. I want to keep the pages using ISR - I want my app to fetch data on server only.
So you did not get me right. My idea was to skip data fetching only on build phase but right after the build finishes I could just trigger the revalidation somehow and this will cause the fetch call on server side. The data will be successfully fetched with one single fetch and the data will be saved in data cache and reused on any page that relies on the same fetch call.

fluid mapleBOT
#

Let me check the knowledge base for you. One moment! :books:

#

I understand your requirement better now. You want to skip data fetching during the build phase and only fetch data on the server-side after the build phase, during the start phase.

Unfortunately, as per the current Next.js design, there's no built-in way to skip data fetching during the build phase for pages that use Incremental Static Regeneration (ISR). The getStaticProps function, which is used for data fetching in ISR, runs at build time and revalidates the data at the intervals specified by the revalidate property.

However, you can potentially work around this by returning an empty object or some default props from getStaticProps during the build phase, and then rely on the revalidation to fetch the actual data after the build phase. Here's an example:

// pages/index.js
export async function getStaticProps() {{
  // Return default props during build phase
  return {{ props: {{ data: null }}, revalidate: 10 }};
}}

// pages/catalog.js
export async function getStaticProps() {{
  // Return default props during build phase
  return {{ props: {{ data: null }}, revalidate: 10 }};
}}

In this setup, getStaticProps returns default props during the build phase, so no fetch requests are made. Then, during the start phase, the revalidate property triggers a revalidation of the data every 10 seconds. You can adjust the revalidate interval to suit your needs.

Please note that this is a workaround and might not be suitable for all use cases. It also relies on the revalidation to fetch the actual data, so there might be a delay before the actual data is displayed on the pages.

#

As of now, Next.js does not provide a built-in way to share fetched data across multiple pages at build time. This is a limitation of the static generation model, where each page is built independently and doesn't share state or memoization with other pages. If making multiple fetch requests to your CMS API during the build process is a concern, you might need to consider other data fetching strategies or frameworks that better suit your needs.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@feral kernel kapa.ai is still learning and improving, please let us know how it did by reacting below

feral kernel
#

@fluid maple I have an idea of potential workaround - I want to make the build command to at first make one single fetch call to the CMS, save the received data in some temporary file and only after this the standard next build procedure comes in, and instead of calling fetch to the CMS in every single page I will just request the data saved in the temporary file.

Is this idea good or not? How could I implement this?

fluid mapleBOT
#

Let me check the knowledge base for you. One moment! :books:

#

Your idea is indeed a viable workaround to avoid making multiple fetch requests to your CMS during the build process. By fetching the data once and saving it to a temporary file before the build process, you can then read from this file during the build process, effectively reusing the fetched data across multiple pages.

Here's a high-level overview of how you could implement this:

  1. Create a script that fetches the data from your CMS and writes it to a temporary file. You can use Node.js's built-in fs module to write to a file. Here's a basic example:
// scripts/fetch-data.js
const fs = require('fs');
const fetch = require('node-fetch');

async function fetchData() {
  const res = await fetch('<http://127.0.0.1:3000/api/products'>);
  const data = await res.json();
  fs.writeFileSync('./temp/data.json', JSON.stringify(data));
}

fetchData();
  1. Run this script before the next build command. You can do this by modifying the build script in your package.json file:
"scripts": {
  "fetch-data": "node scripts/fetch-data.js",
  "build": "npm run fetch-data && next build"
}
  1. In your pages, instead of making a fetch request to your CMS, read the data from the temporary file:
// pages/index.js
import fs from 'fs';
import path from 'path';

export async function getStaticProps() {
  const data = JSON.parse(fs.readFileSync(path.resolve('./temp/data.json'), 'utf-8'));
  return { props: { data }, revalidate: 10 };
}
#

Please note that this is a simplified example and might need to be adjusted to suit your specific use case. For example, you might need to handle errors and edge cases, such as what happens if the fetch request fails or if the temporary file does not exist. Also, keep in mind that this approach adds an extra step to your build process and might increase the build time.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@feral kernel kapa.ai is still learning and improving, please let us know how it did by reacting below

feral kernel
#

@fluid maple Thank you, the problem is solved

fluid mapleBOT
#

Let me check the knowledge base for you. One moment! :books: