Next.js Discord

Discord Forum

Potentional problem in Nextjs data fetching

Unanswered
Prairie yellowjacket posted this in #help-forum
Open in Discord
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:

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 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:
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
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
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