Potentional problem in Nextjs data fetching
Unanswered
Prairie yellowjacket posted this in #help-forum
Prairie yellowjacketOP
Hey guys, I have noticed something through building my app.
I'm using Next 14, and I have async table with pagination, search filters, and many more filters that triggers router.push function.
First problem I noticed is loading, I have loading.tsx which triggers when I render the page first time, but when query params my client data table doesn't know when the fetch is happening. I have tried to add supsense fallback but doesn't work. I would be great if other content is shown but data table when its loading, loading ui shows just like it's displayed on suspense fallback
Second problem I noticed is unwanted fetches, to explain you as better as I can, in my example I'm fetching football players, and with that fetch I also have fetch call for countries, leagues and seasons. Those data shouldn't be fetched I only want to fetch filtered player data.
Does anyone have an idea for those 2 problems? Code structure is in comment:
I'm using Next 14, and I have async table with pagination, search filters, and many more filters that triggers router.push function.
First problem I noticed is loading, I have loading.tsx which triggers when I render the page first time, but when query params my client data table doesn't know when the fetch is happening. I have tried to add supsense fallback but doesn't work. I would be great if other content is shown but data table when its loading, loading ui shows just like it's displayed on suspense fallback
Second problem I noticed is unwanted fetches, to explain you as better as I can, in my example I'm fetching football players, and with that fetch I also have fetch call for countries, leagues and seasons. Those data shouldn't be fetched I only want to fetch filtered player data.
Does anyone have an idea for those 2 problems? Code structure is in comment:
7 Replies
Prairie yellowjacketOP
const PlayersPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const user = await currentUser();
if (!user?.id) {
redirect("/auth/login");
}
const { players, total } = await getPlayers(searchParams, user?.access_token);
const { countries } = await getCountries(user.access_token);
const { leagues } = await getLeagues(user.access_token);
const { seasons } = await getSeasons(user.access_token);
const formatedPlayers: PlayersColumn[] = players.map(
(item: ExtendedPlayer) => {
return {
// ...
};
},
);
return (
<PlayersClient
initialPlayers={formatedPlayers}
total={total}
countries={countries}
leagues={leagues}
seasons={seasons}
/>
);
};export const PlayersClient: React.FC<PlayersClientProps> = ({
total, initialPlayers, countries, leagues, seasons,
}) => {
const [text, setText] = useState(paramsObject.search || "");
const [searchQuery] = useDebounce(text, 750);
useEffect(() => {
if (searchQuery) {
router.push(`?${filtersStringNoPage}&search=${searchQuery}&page=0`);
} else if (searchQuery === "") router.push(`?${filtersString}`);
// eslint-disable-next-line
}, [searchQuery]);
return (
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex w-full items-center gap-4 rounded-[16px] bg-card p-4">
// ...
{seasons && (
<div className="w-full space-y-1">
<label className="text-sm text-muted-foreground">Seasons</label>
<MultiSelect
options={seasonOptions}
onValueChange={setSelectedSeasons}
defaultValue={selectedSeasons}
setMenuOpen={setSeasonsMenuOpen}
menuOpen={seasonsMenuOpen}
/>
</div>
)}
<div className="mb-[2px] flex h-full w-full flex-col items-end gap-2">
<div className="relative w-full">
<Input
placeholder="Search..."
value={text}
onChange={(e) => setText(e.target.value)}
className="rounded-[8px] bg-primary-foreground"
/>
</div>
</div>
</div>
<div className="flex flex-1 gap-x-2 overflow-hidden">
<Filters total={total} limitPerPage={10} resetAll={resetAllFilters} />
<Suspense key={uuidv4()} fallback={<p>Loading...</p>}>
<DataTable
columns={columns}
data={initialPlayers}
total={total}
onRowClick={["id", onRowClick]}
setOpenFilters={setOpenFilters}
/>
</Suspense>
<MetricFilters resetAll={resetAllFilters} />
</div>
</div>
);
}@Prairie yellowjacket Hey guys, I have noticed something through building my app.
I'm using Next 14, and I have async table with pagination, search filters, and many more filters that triggers router.push function.
First problem I noticed is loading, I have loading.tsx which triggers when I render the page first time, but when query params my client data table doesn't know when the fetch is happening. I have tried to add supsense fallback but doesn't work. I would be great if other content is shown but data table when its loading, loading ui shows just like it's displayed on suspense fallback
Second problem I noticed is unwanted fetches, to explain you as better as I can, in my example I'm fetching football players, and with that fetch I also have fetch call for countries, leagues and seasons. Those data shouldn't be fetched I only want to fetch filtered player data.
Does anyone have an idea for those 2 problems? Code structure is in comment:
first problem: you need to use a
second problem: you can use unstable_cache to cache queries that should not rerun when the searchParams changes. see for example
key to force-rerender Suspense: https://nextjs-forum.com/post/1255626619247136919second problem: you can use unstable_cache to cache queries that should not rerun when the searchParams changes. see for example
const query1 = unstable_cache(
async () => console.log("Only run once")
);
async function Page({ searchParams }) {
console.log("Run on all requests");
await query1(); // the callback in `query1` only run once
return ...
}Prairie yellowjacketOP
for first problem I have tried to add a key in suspense boundary but nothing happens...
for the second problem still getting unwanted fetches after changing the code to this:
for the second problem still getting unwanted fetches after changing the code to this:
const getCachedSeasons = unstable_cache(
async (access_token) => getSeasons(access_token),
["my-app-seasons"],
);
async function Page({ searchParams }) {
const { seasons } = await getCachedSeasons(user.access_token);
...@Prairie yellowjacket for first problem I have tried to add a key in suspense boundary but nothing happens...
for the second problem still getting unwanted fetches after changing the code to this:
js
const getCachedSeasons = unstable_cache(
async (access_token) => getSeasons(access_token),
["my-app-seasons"],
);
async function Page({ searchParams }) {
const { seasons } = await getCachedSeasons(user.access_token);
...
first problem: i can't really guess anything from that much info.
second problem: here is a more complete example
second problem: here is a more complete example
import { unstable_cache } from "next/cache";
import { cookies } from "next/headers";
import Link from "next/link";
async function getAccessTokenLength(token: string) {
console.log("This is run");
return token.length;
}
export default async function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
const accessToken = cookies().get("access_token")?.value ?? "";
console.log("token", accessToken);
const tokenLength = await unstable_cache(
() => getAccessTokenLength(accessToken),
[accessToken]
)();
console.log("The main component is run");
const query = Array.isArray(searchParams.query)
? searchParams.query[0]
: searchParams.query;
return (
<div>
<div>tokenLength = {tokenLength}</div>
<div>query = {query ?? "N/A"}</div>
<div>
<Link href="/?query=hello">Click me to change searchParams</Link>
</div>
</div>
);
}Prairie yellowjacketOP
sadly does not work :/ every time I try to search or change some filters, when router push happens to new query seasons fetch again
getSeasons function
const PlayersPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) => {
const user = await currentUser();
if (!user?.id) {
redirect("/auth/login");
}
const { players, total } = await getPlayers(searchParams, user?.access_token);
const { countries } = await getCountries(user.access_token);
const { leagues } = await getLeagues(user.access_token);
// const { seasons } = await getSeasons(user.access_token);
const { seasons } = await unstable_cache(
() => getSeasons(user.access_token),
[user.access_token],
)();
const formatedPlayers: PlayersColumn[] = players.map(
(item: ExtendedPlayer) => {
return {
...
};
},
);
return (
<div className="flex h-screen flex-1 flex-col overflow-hidden pb-4">
<PlayersClient
initialPlayers={formatedPlayers}
total={total}
countries={countries}
leagues={leagues}
seasons={seasons}
/>
</div>
);
};getSeasons function
export const getSeasons = async (
token: string,
): Promise<{ seasons: Season[] }> => {
try {
console.log("getting seasons");
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/seasons`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
throw new Error("Seasons not found");
}
const data = await res.json();
return { seasons: data };
} catch (error) {
console.log("SEASON_FETCH", error);
return { seasons: [] };
}
};@Prairie yellowjacket js
export const PlayersClient: React.FC<PlayersClientProps> = ({
total, initialPlayers, countries, leagues, seasons,
}) => {
const [text, setText] = useState(paramsObject.search || "");
const [searchQuery] = useDebounce(text, 750);
useEffect(() => {
if (searchQuery) {
router.push(`?${filtersStringNoPage}&search=${searchQuery}&page=0`);
} else if (searchQuery === "") router.push(`?${filtersString}`);
// eslint-disable-next-line
}, [searchQuery]);
return (
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex w-full items-center gap-4 rounded-[16px] bg-card p-4">
// ...
{seasons && (
<div className="w-full space-y-1">
<label className="text-sm text-muted-foreground">Seasons</label>
<MultiSelect
options={seasonOptions}
onValueChange={setSelectedSeasons}
defaultValue={selectedSeasons}
setMenuOpen={setSeasonsMenuOpen}
menuOpen={seasonsMenuOpen}
/>
</div>
)}
<div className="mb-[2px] flex h-full w-full flex-col items-end gap-2">
<div className="relative w-full">
<Input
placeholder="Search..."
value={text}
onChange={(e) => setText(e.target.value)}
className="rounded-[8px] bg-primary-foreground"
/>
</div>
</div>
</div>
<div className="flex flex-1 gap-x-2 overflow-hidden">
<Filters total={total} limitPerPage={10} resetAll={resetAllFilters} />
<Suspense key={uuidv4()} fallback={<p>Loading...</p>}>
<DataTable
columns={columns}
data={initialPlayers}
total={total}
onRowClick={["id", onRowClick]}
setOpenFilters={setOpenFilters}
/>
</Suspense>
<MetricFilters resetAll={resetAllFilters} />
</div>
</div>
);
}
Prairie yellowjacketOP
for the first problem, I have a client component which has all that filters and table, I want to persist everything and show loader for table when fetch is happening, code example is in first comment. I was doing some research, partial loading in next 14 is not so much good but it will be better when next 15 releases