next.js fetch is always fetching
Answered
Sage Thrasher posted this in #help-forum
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.133 Replies
Atlantic menhaden
bump
Sage ThrasherOP
anybody..?
maybe try removing one of the revalidate?
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
can you show me the structure of
APIKey
like not the key itself
the structure of that variable
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
this should ideally cache the data
can you create a minimal reproduction
Sage ThrasherOP
i also use
import 'server-only'
in the api key fileSage ThrasherOP
it does locally, but not on vercel
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?
do you have an existing nextjs app hosted on vercel you could add a fetch to, to see if it works for you?
The apps that I've hosted, caching works great in them
Sage ThrasherOP
were you caching directly from the page file?
yes, adding revalidate in fetch api
Sage ThrasherOP
okay 2secs let me try and replicate it
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?
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?
I am not sure about the first one but seconds one counts as one
Sage ThrasherOP
what if its a cache hit?
i presume if the fetch gets from cache instead of making the request, its not a function invocation
Atlantic menhaden
bump
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 thoughSage 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.
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.
Does the fetching you are doing HAVE to be done from the client, does it depend on information only the client can know?
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?
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.
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?
Without
Suspense
anytime you await
any component that is rendered beneath that component in the tree will be blocked until it resolvesYeah 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?
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 hourYeah, 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)
Sage ThrasherOP
ah so the hero would accept a children props then in the server i would do something like this:
<Hero>
<ServerComponent />
</Hero>
<Heading>
<HeroServerComponentWithClientComponentRenderedInside />
</Heading>
Sorry for the verbose name lmao but hopefully you understand what I meant
Sage ThrasherOP
but if the hero is a client component as well?
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
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
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
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
Use a GET Route Handler instead, and cache the GET. Server Actions can't be cached but Route Handlers can
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;
}
This is fetching to an external API outside of your Next application right?
Sage ThrasherOP
yes
What version of next are you using?
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
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 dataOR
use
unstable_cache
(not recommended honestly)Sage ThrasherOP
alright so im probably better of using a route handler
Yes, however this won't reduce your function invocation issue
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
it's probably because you are passing headers, which is request information so it needs to be dynamic to ensure it's there everytime.
Sage ThrasherOP
is there much i can do about this?
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 usecasesThe 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
}
Sage ThrasherOP
yeah its static
import 'server-only';
export const APIKey = {
'backend-api-key': process.env.API_KEY,
'Authorization': process.env.API_AUTHORIZATION
}
Ah okay it is an object
Hmm if it's static, then the issue isn't the headers
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
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
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
Sage ThrasherOP
yeah it needs the entire response, i only sent the data needed
Sounds good
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
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.
Sage ThrasherOP
true, i thought it was a lot further, but thats probably on the other pages
Yeah try to find the page where it is the deepest
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?
Your
useSearchQuery()
is calling useSearchParams()
?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];
}
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
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
Sage ThrasherOP
should i remove it entirely and get searchParams from Page
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
Sage ThrasherOP
around HomeHero?
or around HeroPart now that i moved it into that
Yup
Whatever calls
useSearchQuery/Params
Sage ThrasherOP
if my page.tsx was being rendered on the client, wouldn't i see that api call in the network tab though?
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
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 retrievedWrap it in
Suspense
Suspense is your friend in Next.js/React for all things async
Probably my favorite react API ever.
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
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
)Sage ThrasherOP
ah okay so i could remake the component using the loader and once loaded, render the one without the loader
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)
Sage ThrasherOP
so would i be doing something like this:
or 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>
)
}
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 usersSage ThrasherOP
is there a way i could do the loading.tsx file on
/
without setting it for all routes? i suppose not rightThere 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.
Sage ThrasherOP
You don't mind if i come back here for follow up questions, do you?
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.
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
Yup you got it