Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: Clarify React.cache() usage in generateMetadata() #62162

Open
kevinmitch14 opened this issue Feb 16, 2024 · 6 comments
Open

Docs: Clarify React.cache() usage in generateMetadata() #62162

kevinmitch14 opened this issue Feb 16, 2024 · 6 comments
Assignees
Labels
Documentation Related to Next.js' official documentation.

Comments

@kevinmitch14
Copy link
Contributor

What is the improvement or update you wish to see?

This part of the docs, gives the impression that cache() can be used in Metadata.

Is there any context that might help us understand?

From Next docs:

Good to know:
fetch requests are automatically memoized for the same data across generateMetadata, generateStaticParams, Layouts, Pages, and Server Components. React cache can be used if fetch is unavailable.

However in the docs on www.react.dev, it states that cache() can only be used in components - here.

React only provides cache access to the memoized function in a component. When calling getUser outside of a component, it will still evaluate the function but not read or update the cache.
This is because cache access is provided through a context which is only accessible from a component.

I'm not 100% on the Metadata API and how it works with React.cache() but I've been debugging an issue in relation to this and it seems that, indeed, cache is not applied in Metadata. Whereas, when using fetch() caching/deduping is applied.

It would be great to get clarification on the expected behaviour here. Happy to create a PR for updating docs but would like to understand more before doing so.

Does the docs page already exist? Please link to it.

https://nextjs.org/docs/app/building-your-application/optimizing/metadata

@delbaoliveira
Copy link
Contributor

delbaoliveira commented Feb 23, 2024

Hello @kevinmitch14 👋🏼

I haven't been able to reproduce this with generateMetadata() on the latest Next.js. For example, 'fetching x' is logged only once when using cache, but removing it will log it twice.

const getX = cache(async () => {
  console.log('fetching x');
});

export function generateMetadata() {
  const x = getX();
  return { title: '...' };
}

export default function Page() {
  const x = getX();

While the React docs talk about it in being in components, I believe in Next.js, generateMetadata and rendering share the same context.

Are you able to provide a minimum reproduction?

@delbaoliveira delbaoliveira self-assigned this Feb 23, 2024
@kevinmitch14
Copy link
Contributor Author

Have been playing around with this, not sure if I remember the exact issue I was encountering but here's a repro - https://github.com/kevinmitch14/nextjs-metadata-cache

I've narrowed it down to the presence of loading.tsx it seems. When a page has a <Link /> that links to a route with a loading.tsx, the cached function is being called during the prefetch I believe.


With loading.tsx in /blog and visiting /

Screenshot 2024-02-23 at 15 54 07

Without loading.tsx and visiting /

Screenshot 2024-02-23 at 15 56 03

Root layout:

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  unstable_noStore();

  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="flex">
          <div className="bg-red-200 w-60 flex flex-col h-screen">
            <Link href={"/blog"}>Blog</Link>
            <Link href={"/news"}>News</Link>
          </div>
          {children}
        </div>
      </body>
    </html>
  );
}

@kevinmitch14
Copy link
Contributor Author

kevinmitch14 commented Feb 23, 2024

It also seems that the generateMetadata() is called on every navigation. The function is located in the root layout. So I would expect that it only gets called once. It seems in the repro, that when you click on the links, the metadata information is updated.

Steps to reproduce:

  1. Visit /
  2. Click link to visit /blogor /news
  3. Look at the metadata title, the time will be different on each navigation.

I've raised this in #62165 also.


Even when the generateMetadata function is called due to prefetch with loading.tsx in /blog (if im understanding correctly), it is still called again, when actually navigating to /blog

@kevinmitch14
Copy link
Contributor Author

kevinmitch14 commented Feb 23, 2024

Doing a bit more research, this is due to streaming?

Next.js will wait for data fetching inside generateMetadata to complete before streaming UI to the client. This guarantees the first part of a streamed response includes tags.

In order to show loading.tsx, we need the metadata information to be present. And by default, Nextjs will load the nearest loading.tsx for dynamic routes?

So when Nextjs prefetches the nearest loading.tsx, it calls generateMetadata() as part of this prefetch?

This is all making sense to me but it still confuses me why generateMetadata() in a root layout.tsx is called multiple times and on every navigation. Should this be persisted due to it being in a layout?

@delbaoliveira
Copy link
Contributor

Thank you for the reproduction! Looking into the multiple calls when loading.tsx is present.

It also seems that the generateMetadata() is called on every navigation. The function is located in the root layout.

I'd expect this to happen, consider a title template, we'd need to call the parent generateMetadata() to know what template is.

@kevinmitch14
Copy link
Contributor Author

kevinmitch14 commented Feb 28, 2024

Thanks for the response.

Hmm, yeah. My impression was that anything in layout.tsx will remain the same on navigations etc. But the template use-case does make sense.

a shared layout is not re-rendered during navigation

Can the template pattern be achieved by having a base metadata in the layout.tsx and then overwrite this in page.tsx for example? Using ResolvingMetadata?

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json())
 
  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || []
 
  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

For context, we have a multi-tenant application, and would like to show the tenants logo as the favicon/icon. Hitting the DB to get this on every navigation is not very realistic. I am not using fetch either.

I guess we can use something like unstable_cache to query the logo but I don't really want to go messing with caching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation Related to Next.js' official documentation.
Projects
None yet
Development

No branches or pull requests

2 participants