Next.js Discord

Discord Forum

Double lookup despite using cache

Unanswered
Bombay-duck posted this in #help-forum
Open in Discord
Bombay-duckOP
In this code resolvePlayer is being called twice, when player was not found in the resolvePlayer method, is there a way to fix that?
import { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { cache } from 'react';
import redis from '~/lib/redis';
import { sanitizeString } from '~/lib/security';

import { fetchAllPlayerStatsAction } from '~/app/private-server-actions/rankingActions';

type PageProps = {
  params: Promise<{ player: string }>;
};

// Function to resolve player name and UUID with caching
const resolvePlayer = cache(async (player: string): Promise<PlayerData> => {
  //code logic
  return { /user/ };
});

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { player } = await params;
  const playerData = await resolvePlayer(player);

  return {
    title: `${playerData.name}`,
    description: ``,
    keywords: [''],
  };
}

export default async function GraczPage({ params }: PageProps) {
  const { player } = await params;
  const playerData = await resolvePlayer(player);
  
  // If player doesn't exist, redirect to ranking page with notification
  if (!playerData.exists) {
    redirect('/');
  }
  
  // Pre-fetch all player stats server-side with ISR caching
  const playerStats = await fetchAllPlayerStatsAction(playerData.uuid);
  
  return (
    <>
    //view
    </>
  );
}

29 Replies

Western paper wasp
resolvePlayer is called both in generateMetadata and in the page component itself. These are two different render invocations, and cache() doesn’t share the result between them.
You can move data fetching to a single place, for example only in the page and pass it into metadata, or accept this behavior as normal. That’s how App Router and Metadata work.
For full control, use an external cache such as Redis or a database instead of react.cache
Western paper wasp
The pattern from the docs looks similar, but react.cache() isn’t shared between generateMetadata and page. Those are different render contexts. The docs show a memoized fetch, but it will still run twice, once for metadata and once for the page. This is expected behavior in the App Router.
If you need a single real request, move the data fetching into the page and pass it into metadata, or use an external cache such as Redis or a database instead of react.cache.
@Western paper wasp The pattern from the docs looks similar, but `react.cache()` isn’t shared between `generateMetadata` and `page`. Those are different render contexts. The docs show a memoized fetch, but it will still run twice, once for metadata and once for the page. This is expected behavior in the App Router. If you need a single real request, move the data fetching into the page and pass it into metadata, or use an external cache such as Redis or a database instead of `react.cache`.
Cinnamon Teal
I don't think that's correct.

The whole purpose of cache() from React is to cache (memoize) the function, so that the function body doesn't run multiple times for the same render. And both metadata and the page functions run in the same render, so the fetch will only run one time, thanks to the cache().

For example, I tested with the below setup now, and the console log only gets printed one time. Not twice. Even though the function is called in metaData and as well as in the page.

import { Metadata } from "next"
import { cache } from "react"

const getSomething = cache(async () => {
  await new Promise((resolve) => setTimeout(resolve, 2000))
  console.log("Function executed.")
})

export async function generateMetadata(): Promise<Metadata> {
  await getSomething()
  return {
    description: "Description",
    title: "Title",
  }
}

export default async function TestPage() {
  await getSomething()
  return <h1>Hello, World!</h1>
}

And if I remove the cache, then the console log gets printed twice.

Happy to be corrected if I am missing something.
Western paper wasp
You’re right that react.cache() does memoize a call within a single render pass, and in a simple generateMetadata + page example it can look like a single call. My previous reply was about real world behavior in the App Router, where generateMetadata and page are not guaranteed to share the exact same render context in all modes (RSC, streaming, prefetch, ISR, edge). In those cases, the react.cache() result may not be reused, and the request can be executed again. So if you need strictly one real network request, the safe option is to move the fetch into page and pass the data into metadata, or use an external cache (Redis/DB) instead of relying on react.cache().
Western paper wasp
react.cache() caches only within a single render,generateMetadata() and page can be rendered separately (different contexts, streaming, ISR, prefetch). That’s why the function may be called twice, and this is normal in the App Router.
How to guarantee a single real request:
- Perform the request only in page
- Reuse the result for both the page and metadata (via arguments/object)
- Or use an external cache (Redis/DB)

react.cache() is not a guarantee of a single request. For 100% certainty, use a single fetch or an external cache
Western paper wasp
If you want only one real call, fetch once in the page and derive metadata from the result.

export default async function GraczPage({ params }: PageProps) {
const { player } = await params;
const playerData = await resolvePlayer(player);

if (!playerData.exists) {
redirect('/');
}

return (
#Unknown Channel
{/* view */}
</>
);
}

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { player } = await params;

return {
title: player,
};
}
Bombay-duckOP
"you can use React's cache function to memoize the return value and only fetch the data once."
this is being used, but logs clearly show resolvePlayer executed twice
Bombay-duckOP
my guess is that its a bug where redirect causes generateMetadata to be executed without cache
@Bombay-duck my guess is that its a bug where redirect causes generateMetadata to be executed without cache
Cinnamon Teal
Do you fetch the players inside the root page as well?

I mean after redirecting to /, does that page fetch the players?

Edit: Ah no, I don't think you will be doing it since the player name is grabbed from the slug. For a second I thought, maybe the resolvePlayer function maybe getting called from the / page as well.

Honestly not sure what could be wrong. If you checked the example I showed above, which works fine, your setup is similar to that as well.
that would solve the mystery
because it redirects to the search page which autofills the name, but if the name is incorrect it could call the check again, but i doubt it
ill analyze it
@riský btw guys, darkstar is correct and if it doesnt work like this then imo its a bug...
Bombay-duckOP
ye i checked and redirect doesnt call the method
Longtail tuna
it;s not a bug
React.cache is scoped to render trees or whatever it;s called
@Longtail tuna React.cache is scoped to `render trees` or whatever it;s called
Bombay-duckOP
so what doest that mean
it should cache like the docs say
how else you want to cache it