Next.js Discord

Discord Forum

next.js fetch is always fetching

Answered
Sage Thrasher posted this in #help-forum
Open in Discord
Avatar
Sage ThrasherOP
import HomeHero from "@/layouts/home/HomeHero";
import ServerSection from "@/layouts/home/parts/ServerSections";
import { APIKey } from "@/lib/data";


export const revalidate = 300;

export default async function Page() {
  const response = await fetch(`${process.env.API_ENDPOINT}/guilds/home`, {
    headers: APIKey,
    next: {
      revalidate: 300
    }
  });

  const data = await response.json();


Every single time i refresh the page, this request is made and it's not being cached meaning my function invocations are increasing significantly on vercel. When running this on localhost it used cache but running it on vercel doesn't
Answered by Plague
You need to wrap any component that uses useSearchParams() in a Suspense boundary, otherwise it bubbles up the tree until it hits a Suspense boundary causing anything it bubbles through to be client-side rendered.
View full answer

133 Replies

Avatar
Atlantic menhaden
bump
Avatar
Sage ThrasherOP
anybody..?
Avatar
maybe try removing one of the revalidate?
Avatar
Sage ThrasherOP
i originally didnt have the revalidate in the fetch request, i added that after to see if it fixed the issue which it didnt
Avatar
can you show me the structure of APIKey
like not the key itself
the structure of that variable
Avatar
Sage ThrasherOP
ive now moved this fetch into a server action which works, however afaik this uses function invocations which is why i wanted to use the fetch directly in the server component because i dont believe cache hits increase function invocations

export const APIKey = {
    'backend-api-key': process.env.API_KEY,
    'Authorization': process.env.API_AUTHORIZATION
}
i also have other stuff in this same file if you want me to send that
Avatar
this should ideally cache the data
can you create a minimal reproduction
Avatar
Sage ThrasherOP
i also use import 'server-only' in the api key file
Avatar
Sage ThrasherOP
it does locally, but not on vercel
Avatar
Sage ThrasherOP
im not sure how to do this because i'd have to host it on vercel to see if its got the same issue.
do you have an existing nextjs app hosted on vercel you could add a fetch to, to see if it works for you?
Avatar
The apps that I've hosted, caching works great in them
Avatar
Sage ThrasherOP
were you caching directly from the page file?
Avatar
yes, adding revalidate in fetch api
Avatar
Sage ThrasherOP
okay 2secs let me try and replicate it
Avatar
Sage ThrasherOP
thats weird, the cache works on the replicated version
i moved this API key to its own file and i think thats fixed it?
i had other methods inside this function which i was reusing, maybe that caused the revalidation each time
also 2 things
1. does using server actions in server components count as a function invocation?
2. does using fetch in server components count as a function invocation if its to an external api?
Avatar
I am not sure about the first one but seconds one counts as one
Avatar
Sage ThrasherOP
what if its a cache hit?
Image
i presume if the fetch gets from cache instead of making the request, its not a function invocation
Avatar
Atlantic menhaden
bump
Avatar
Yes Server Actions count as a function invocation, a server action is an Serverless Function (Route Handler) under the hood
fetch itself does not count as a function invocation as it isn't a Serverless or Edge function, the Serverless Function is the page that calls that fetch. It does contribute to the total execution time (compute) of the function though
Avatar
Sage ThrasherOP
right now i use a lot of server actions for getting data and i mix it with react-query. the reason i use server actions for get is because its easier than route handlers, and making a fetch call to a route handler also increases function invocations.
as you can see, i've managed to reach 1.24mil function invocations, and im not quite sure how i could possible limit this as much as possible.
Avatar
Does the fetching you are doing HAVE to be done from the client, does it depend on information only the client can know?
Avatar
Sage ThrasherOP
well its quite far down the component tree im not quite sure how i could do it on the server
one of the fetches i perform on all pages doesnt require auth for the data because its not sensitive but its ran on the client because its far down the component tree
unless i could put a server component inside a client component and make a new component for it?
Avatar
You could instead make a Server Component wrapper around the client component fetch the data their wrap it in suspense and pass the data to the client, slight overhead but is worth it in my opinion.
Avatar
Sage ThrasherOP
ive never used suspense before. this part of the data doesnt load in with the main page its only a "side bit" so would it mean i'd have to wait for this to fetch for everything else to render? also, how would i go about doing this, because this is on every single page so would i have to parse this down like 10 different components?
Avatar
Without Suspense anytime you await any component that is rendered beneath that component in the tree will be blocked until it resolves
Yeah it depends on the structure of your application, the client component that you are currently fetching data in, is imported into another client componnet I'm assuming?
Avatar
Sage ThrasherOP
yes
Page -> Header (client) -> Hero (client) -> Component where Fetch is
also, ive been having a lot of issues with next.js caching in the fetch method, one of my routes has 1 day revalidate (next: { revalidate: 86400 }) but the request is made everytime, and if i spam refresh my home page, a request is made to the api which caches data for an hour
Avatar
Yeah, so you can still achieve what I stated by creating a wrapper around the hero that fetches the data, and passing that wrapper with the hero rendered inside to the header component through the children property see this [documentation](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#supported-pattern-passing-server-components-to-client-components-as-props)
Avatar
Sage ThrasherOP
ah so the hero would accept a children props then in the server i would do something like this:

<Hero>
  <ServerComponent />
</Hero>
Avatar
<Heading>
<HeroServerComponentWithClientComponentRenderedInside />
</Heading>
Sorry for the verbose name lmao but hopefully you understand what I meant
Avatar
Sage ThrasherOP
but if the hero is a client component as well?
Avatar
That's what I'm saying, you would render that Hero client component inside the HeroServerComponent which is the new wrapper you should make the fetch the data within
Then that HeroServerComponent would be passed to the Header component via children
Avatar
Sage ThrasherOP
i could move header into its own separate component as its a fragment right now:
// Header.tsx
return (
  const [state, setState] = ...
  <>
    <Header state />
    <div>
      <Hero state setState />
    </div>
  </>
)
then render the hero into the server component
actually, maybe not because it uses a state
Avatar
Yeah it won't work then, you cannot import a Server Component into a client component
With your current architecture I can see why you are fetching from the client, it's not a bad move and is sometimes necessary for certain use cases like this
Avatar
Sage ThrasherOP
yeah, the issue is its on every single page so it can add up
and considering the fact that this route is the one where the fetch cache isnt working
every refresh a server action is called which fetches the api with 1day revalidation
Avatar
Use a GET Route Handler instead, and cache the GET. Server Actions can't be cached but Route Handlers can
Avatar
Sage ThrasherOP
react-query does cache the server action when navigating between pages (but ofc a refresh gets rid of this cache)
but the GET fetch inside the server action isnt getting cached
and i dont know why
'use server';

import { APIKey } from "@/lib/data/APIKey";
import type { TagData } from "@/typing";

export async function fetchTrendingTags(
    { limit = Number.POSITIVE_INFINITY, offset = 0 }:
    { limit?: number, offset?: number } = {}
): Promise<TagData[]> {
    const url = new URL(`${process.env.API_ENDPOINT}/tags/trending`);
    if (limit) url.searchParams.set('limit', limit.toString());
    if (offset) url.searchParams.set('offset', offset.toString());

    const response = await fetch(url.toString(), {
        headers: APIKey,
        next: {
            revalidate: 86400
        }
    });
    if (!response.ok) return [];

    const data = await response.json();
    if (typeof data === 'undefined' || !Array.isArray(data)) return [];

    return data;
}
Avatar
This is fetching to an external API outside of your Next application right?
Avatar
Sage ThrasherOP
yes
Avatar
What version of next are you using?
Avatar
Sage ThrasherOP
├─┬ geist@1.3.1
│ └── next@14.2.15 deduped
├─┬ next-auth@5.0.0-beta.22
│ └── next@14.2.15 deduped
├─┬ next-client-cookies@1.1.1
│ └── next@14.2.15 deduped
├─┬ next-plausible@3.12.1
│ └── next@14.2.15 deduped
└── next@14.2.15
Avatar
Next states in their documentation that fetch requests are not cached when used inside a Server Action or a POST request Route Handler, so you should change to a GET Route Handler if you want to cache that data
OR
use unstable_cache (not recommended honestly)
Avatar
Sage ThrasherOP
alright so im probably better of using a route handler
Avatar
Yes, however this won't reduce your function invocation issue
Avatar
Sage ThrasherOP
however, i have the same thing with my home page where the data sometimes uses the cached but sometimes doesnt
export const revalidate = 3600;

export default async function Page() {
  const response = await fetch(`${process.env.API_ENDPOINT}/guilds/home`, {
    headers: APIKey,
    next: {
      revalidate: 3600
    }
  });

  const { featured, topVoted, trendingNew, recentlyVoted, randomGuilds, topTagGuilds } = await response.json();


and this is my home page fetch
Avatar
it's probably because you are passing headers, which is request information so it needs to be dynamic to ensure it's there everytime.
Avatar
Sage ThrasherOP
is there much i can do about this?
Avatar
Honestly, only solution that I know of is unstable_cache. I use this heavily in my applications since I can't use the fetch API in my usecases
The API Key you are passing, is it static? How are you grabbing that value?
Also the syntax of your fetch seems off, unless the APIkey is an object your fetch should be something like
headers: {
Authorization/Bearer: APIKey
}
Avatar
Sage ThrasherOP
yeah its static
import 'server-only';

export const APIKey = {
    'backend-api-key': process.env.API_KEY,
    'Authorization': process.env.API_AUTHORIZATION
}
Avatar
Ah okay it is an object
Hmm if it's static, then the issue isn't the headers
Avatar
Sage ThrasherOP
i switched to this and seems to work
'use server';

import { APIKey } from "@/lib/data/APIKey";
import { unstable_cache } from "next/cache";

export const getGuildsHome = unstable_cache(async () => {
    const response = await fetch(`${process.env.API_ENDPOINT}/guilds/home`, {
        headers: APIKey
    });
    return response.json();
}, ['guilds-home'], { revalidate: 3600 });
i did read that nextjs doesnt cache fetch inside server actions, however when testing locally, it did work which is why i continued to use it so im not sure why this was
Avatar
Yeah the Server Action I understand why it didn't work, but the page not fetch not working threw me off.

One thing to note, are you using that entire response? If not, I recommended stripping out the parts your application doesn't need otherwise you will be caching unnecessary amounts of data
Avatar
Sage ThrasherOP
yeah it needs the entire response, i only sent the data needed
Avatar
Sounds good
Avatar
Sage ThrasherOP
do you know another way i could make the client fetch server or am i pretty much stuck with that one because its inbetween 2 components needing the same state
"use client";

import { useState, type ReactNode } from "react";
import { useSearchQuery } from "@/hooks/useSearchQuery";
import { Header } from "@/components/layout/Header";
import { HeroPart } from "@/components/hero/HeroPart";

export default function HomeHero(props: { heroText?: string, children?: ReactNode } = {}) {
  const searchParams = useSearchQuery();
  const [showBg, setShowBg] = useState<boolean>(false);

  return (
    <>
      <Header bg={showBg} />
      <div className="flex flex-col">
        <HeroPart searchParams={searchParams} setIsSticky={setShowBg} heroTitle={props.heroText}>
          {props.children}
        </HeroPart>
      </div>
    </>
  );
}

the fetch is inside this HeroPart
this component is imported directly from the page.tsx
Avatar
Wait this HomeHero component is imported directly into the page.tsx?
On every page?
Okay, but it only ever gets imported into the Page.tsx, that means you can just pass that data down only two components. That isn't that deep in the tree
Two components to drill through is nothing IMO.
Avatar
Sage ThrasherOP
true, i thought it was a lot further, but thats probably on the other pages
Avatar
Yeah try to find the page where it is the deepest
Avatar
Sage ThrasherOP
so if theres only 1 element in this hero which needs a loader (the entire hero needs to be loaded because it has a search bar and page title), how could i make this only part have the loader?
Avatar
Your useSearchQuery() is calling useSearchParams()?
Avatar
Sage ThrasherOP
yeah, i've just moved it into the HeroPart
export function useSearchQuery(): [string, (inp: string, force?: boolean) => void, () => void] {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const query = searchParams.get('query');
  const [search, setSearch] = useState<string>(decode(query));

  useEffect(() => {
    setSearch(decode(query));
  }, [query]);

  const updateParams = (inp: string, commitToUrl = false) => {
    setSearch(inp);

    if (!commitToUrl) return;
    const newParams = new URLSearchParams(searchParams.toString());
    if (inp.length === 0) {
      newParams.delete('query');
    } else {
      newParams.set('query', inp);
    }

    if (!inp) {
      window.history.pushState(null, '', `${pathname}?${newParams.toString()}`);
    } else {
      router.replace(`/servers?${newParams.toString()}`);
    }
  };

  const onUnFocus = (newSearch?: string) => {
    updateParams(newSearch ?? search, true);
  };

  return [search, updateParams, onUnFocus];
}
Avatar
lmao, that has been the issue this entire time
Your entire page.tsx is being client-side rendered right now
that's why it's always fresh
Avatar
You need to wrap any component that uses useSearchParams() in a Suspense boundary, otherwise it bubbles up the tree until it hits a Suspense boundary causing anything it bubbles through to be client-side rendered.
Answer
Avatar
Sage ThrasherOP
should i remove it entirely and get searchParams from Page
Avatar
Unneccessary if the page doesn't require the params, cause it'l make your page dynamically rendered, eseentially causing the same thing.
Just add Suspense
Avatar
Sage ThrasherOP
around HomeHero?
or around HeroPart now that i moved it into that
Avatar
Yup
Whatever calls useSearchQuery/Params
Avatar
Sage ThrasherOP
if my page.tsx was being rendered on the client, wouldn't i see that api call in the network tab though?
Avatar
Always need to understand the APIs you are using inside of Next.js, almost all of them affect the rendering model in some way shape or form
probably not cause it still gets prerendered on the server
Avatar
Sage ThrasherOP
ah
also, for that data which needs fetching inside the HeroPart, how would i add a loader for it if im fetching it on the server? or would it not matter if its cached because of how quickly it would be retrieved
Avatar
Wrap it in Suspense
Suspense is your friend in Next.js/React for all things async
Probably my favorite react API ever.
Avatar
Sage ThrasherOP
how does suspense actually work? would it still render instantly or? because i do want the hero to render, just 1 part of it should display a loader
Avatar
Suspense shows a fallback (if you specify one, which you should IMO) until the component resolves then it shows the component. Component resolves after it's async operations have finished (aka the await)
Avatar
Sage ThrasherOP
ah okay so i could remake the component using the loader and once loaded, render the one without the loader
Avatar
Yup, that is exactly what I do with Suspense I create the same HTML structure without the async operations and use that as the fallback shell then replace it with the real component afterwards.
to the user it almost looks like nothing even happened (besides the search bar being disabled, muted, and slightly pulsing so they know it isn't ready to be used yet)
Avatar
Sage ThrasherOP
so would i be doing something like this:
export async function Page() {
  const tags = await fetchTags(); // will be fetch request

  return (
    <Suspense fallback={<HeroPartLoader/>}
      <HeroPart>
        <div>
          {tags.map....}
        </div>
      </HeroPart>
    </Suspense>
  )
}


or something like this
export async function Page() {
  const tags = await fetchTags(); // will be fetch request

  return (
    <Suspense fallback={<HeroPartLoader/>}
      <HeroPart tags={tags} />
    </Suspense>
  )
}
Avatar
Yeah either works, second is cleaner. Be sure to also have a loading.tsx/jsx file wrapping that page component as well since it fetches data, so you can display a instant loading UI for users
Avatar
Sage ThrasherOP
is there a way i could do the loading.tsx file on / without setting it for all routes? i suppose not right
Avatar
There is a way, admittedly annoying but there is a way using Route Groups. Open a new forum post if you have questions about that so that this one doesn't get cluttered with unrelated questions/responses
I also do this in my application.
Remember to mark this as solution for this thread.
Avatar
Sage ThrasherOP
You don't mind if i come back here for follow up questions, do you?
Avatar
I don't, but, you should really open up new forum posts for that. You can tag me in them though if you'd like cause we can always view this one for context.
Avatar
Sage ThrasherOP
ahh wait, i misunderstood, you put the fetch inside a component, then that component goes inside the suspense and returns the target component
Avatar
Yup you got it