Next.js Discord

Discord Forum

Suspense Usage and loading.tsx

Answered
Caspian Nightworth posted this in #help-forum
Open in Discord
I don't seem to be understanding the usage behind Suspense and loading.tsx. I have a component in my header that does a fetch to get some information. I have a loading.tsx file within this folder which, if my understanding is correct will allow for me to have another loading.tsx file for another component that will display other content in the "content" area of my page (I hope this makes sense).

|
| - app
| | - (header)
| | | - _components
| | | | - appVersion
| | | | | - appVersion.tsx
| | | | | - loading.tsx

The contents of my two files are as follows:

appVersion.tsx
"use client";

import { Suspense, useEffect, useState } from "react";
import Loading from "./loading";

export function AppVersion() {
  const [version, setVersion] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchVersion() {
      try {
        const res = await fetch("/api/version");

        // Delay for testing
        await new Promise((resolve) => setTimeout(resolve, 5000));

        const data = await res.json();
        setVersion(data.version);
      } catch (error) {
        console.error("Error fetching version:", error);
        setError("Failed to fetch version");
      }
    }

    void fetchVersion();
  }, []);

  return version && <Suspense fallback={<Loading />}> v{version}</Suspense>;
}


loading.tsx
import { Skeleton } from "@/components/ui/skeleton";

export default function Loading() {
  return <Skeleton className="w-25 h-14" />;
}


I am not seeing my ShadCN skeleton while this version is being fetched. Where am I going wrong in my understanding?
Answered by Julienng
Hi, I followed the current thread and I just created an example to explain how suspense works

By following the thread, I saw that you are fetching using useEffect() and this is a quite difficult exercise to get right.

https://codesandbox.io/p/devbox/wispy-butterfly-dcddqw

You have 3 examples in there, 1 using a server only technique, the two others using a tool called react-query.

React-Query is a library that helps you to sync state from the server.
The lib is way easier than a useEffect and handles all the race condition for you.

You can follow the code in the folder:
- p1 (/p1) for server only
- p2 (/p2) using useSuspenseQuery()
- p3 (/p3) using useQuery and a route /api/version similar to your current setup

I hope this three example can help, and I'm available if you have questions 🙂
View full answer

111 Replies

@Caspian Nightworth loading and suspense will only work for server components
where you fetch data in an async function and use it
with client components, better to use state for loading states
your component needs to resolve a promise for suspense to use it
is it not doing that in my useEffect?
I honestly don't know how to use states or anything. i'm new to front end development as i've been primarily a backend developer and didn't need to worry about any of this stuff
@Caspian Nightworth is it not doing that in my useEffect?
no no, so a useEffect is client js.. so it will already be on the users device when its running
idek what htmx is, lol. my backend languages of choice are PHP and NodeJs
oh lol
anyways
in your case
do this
const [loading, setLoading] = useState(false);
and inside your useEffect, you will set Loading to true at the start and false at the end
and use that to show like a loading ui
@Caspian Nightworth
makes sense?
yeah, i believe i follow. though the testing of this code didn't show like a loading ui
  useEffect(() => {
    setLoading(true);

    async function fetchVersion() {
      try {
        const res = await fetch("/api/version");

        // Delay for testing
        await new Promise((resolve) => setTimeout(resolve, 5000));

        const data = await res.json();
        setVersion(data.version);
      } catch (error) {
        console.error("Error fetching version:", error);
        setError("Failed to fetch version");
      }
    }

    void fetchVersion();
    setLoading(false);
  }, []);
i have in my useEffect a 5 second delay
where you see the normal data first, and then the loading screen appears?
@Caspian Nightworth i have in my useEffect a 5 second delay
that shld be accounted for.. is the loading state being shown as false before it finishes?
oh wait
i see the issue
void fetchVersion().then(() => {
   setLoading(false);
})


@Caspian Nightworth
so basically, you have an await in there, and hence your function itself is a promise yes.. but its not being waited for
with a .then() you are chaining for after it finishes its stuff
@Caspian Nightworth Click to see attachment
i honestly have 0 clue whats going on
xD
the version is what is being requested in that video
did you try the .then stuff btw?
i am now
@Caspian Nightworth i am now
ok, also another thing
see the const[loading... part?
set it to true at the start, since you want to show the loading state as soon as the component renders, this will fix any delays
you can then also remove this
setLoading(true);
since loading is already true
i hope im not like....confusing you lol
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    loading = true;

    async function fetchVersion() {
      try {
        const res = await fetch("/api/version");

        // Delay for testing
        await new Promise((resolve) => setTimeout(resolve, 5000));

        const data = await res.json();
        setVersion(data.version);
      } catch (error) {
        console.error("Error fetching version:", error);
        setError("Failed to fetch version");
      }
    }

    void fetchVersion().then(() => {
      setLoading(false);
    });
  }, []);

like this?
remove loading = true lol
i am totally lost
uhh one sec
fuck
 const [loading, setLoading] = useState(true);

  useEffect(() => {

    async function fetchVersion() {
      try {
        const res = await fetch("/api/version");

        // Delay for testing
        await new Promise((resolve) => setTimeout(resolve, 5000));

        const data = await res.json();
        setVersion(data.version);
      } catch (error) {
        console.error("Error fetching version:", error);
        setError("Failed to fetch version");
      }
    }

    void fetchVersion().then(() => {
      setLoading(false);
    });
  }, []);
there you go
yeah, still nothing 😦
you dont see the delay?
i see the delay. it is still there, but i'm not seeing my loading Skeleton
appVersion.tsx
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
"use client";

import { Suspense, useEffect, useState } from "react";
import { Loading } from "./loading";

export function AppVersion() {
  const [version, setVersion] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchVersion() {
      try {
        const res = await fetch("/api/version");

        // Delay for testing
        await new Promise((resolve) => setTimeout(resolve, 5000));

        const data = await res.json();
        setVersion(data.version);
      } catch (error) {
        console.error("Error fetching version:", error);
        setError("Failed to fetch version");
      }
    }

    void fetchVersion().then(() => {
      setLoading(false);
    });
  }, []);

  return (
    <div className="flex items-center justify-center">
      {version && <Suspense fallback={<Loading />}> v{version}</Suspense>}
      {error && <Suspense fallback={<Loading />}>{error}</Suspense>}
    </div>
  );
}

loading.tsx
import { Skeleton } from "@/components/ui/skeleton";

export function Loading() {
  return (
    <div className="flex items-center justify-center">
      <Skeleton className="h-4 w-[200px] bg-zinc-500" />
    </div>
  );
}
oh i didnt mention ig, one sec
  return (
    <div className="flex items-center justify-center">
      {version &&
              (loading ? <Loading /> : v{version}
      )}
    </div>
  );
@Caspian Nightworth its gonna be something like that, if vscode shows you a syntax error.. send me that lol.. im bad at just discord coding pretty nested stuff lol
but the same will be with your error stuff
sure is complaining
oh i see it
i need to remove the <Suspense>
remove the suspense lol
yea
also you forgot the :
we can do the error stuff later
uhhhhhhhhh, ok so for the v{version}
change it to this
`
v${version}`
merge them in one line lol
discord formatting xD
fixed the formatting, but the Skeleton still isn't displaying
@Caspian Nightworth fixed the formatting, but the Skeleton still isn't displaying
thats unusual, can you console log version and loading real quick
log it where you set loading to false
@Caspian Nightworth Click to see attachment
looks like the issue is with your version
can you log data?
i forgot to wait 5 seconds to take the screenshot
@Caspian Nightworth Click to see attachment
remove the void + can you hover over the vscode warning and show me what it says
@Caspian Nightworth Click to see attachment
Did you remove the void bte
yes
Something abt your setup feels wrong, make a replicate at codesandbox (Google it) and send the link here
Just a minimal replication btw
So remove the data fetching and just set a hardcoded value
Etc
ok
Hi, I followed the current thread and I just created an example to explain how suspense works

By following the thread, I saw that you are fetching using useEffect() and this is a quite difficult exercise to get right.

https://codesandbox.io/p/devbox/wispy-butterfly-dcddqw

You have 3 examples in there, 1 using a server only technique, the two others using a tool called react-query.

React-Query is a library that helps you to sync state from the server.
The lib is way easier than a useEffect and handles all the race condition for you.

You can follow the code in the folder:
- p1 (/p1) for server only
- p2 (/p2) using useSuspenseQuery()
- p3 (/p3) using useQuery and a route /api/version similar to your current setup

I hope this three example can help, and I'm available if you have questions 🙂
Answer
thank you, i'll take a look
@Julienng I have a version of what you provided for p3
appVersion.tsx
import { Suspense } from "react";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { VersionCard } from "./VersionCard";

export const AppVersion = () => {
  return (
    <div className="flex items-center justify-center">
      <Suspense fallback={<LoadingSkeleton />}>
        <VersionCard />
      </Suspense>
    </div>
  );
};

LoadingSkeleton.tsx
import { Skeleton } from "@/components/ui/skeleton";

export const LoadingSkeleton = () => {
  return (
    <div className="flex items-center justify-center">
      <Skeleton className="h-4 w-[200px] bg-zinc-500" />
    </div>
  );
};

VersionCard.tsx
"use client";

import { useQuery } from "@tanstack/react-query";
import { LoadingSkeleton } from "./LoadingSkeleton";

interface VersionApi {
  version: string;
}

async function fetchVersion(): Promise<VersionApi | Error | undefined> {
  const res = await fetch("/api/version");

  if (res.ok) {
    const response = (await res.json()) as VersionApi;
    const data: VersionApi = Promise.resolve(response);

    console.log(data);

    return data;
  } else {
    throw new Error("Failed to fetch version");
  }

  return undefined;
}

export const VersionCard = () => {
  const { data: version, isLoading } = useQuery({
    queryFn: fetchVersion,
    queryKey: [],
  });

  if (isLoading) {
    return <LoadingSkeleton />;
  }

  return (
    <div className="flex items-center justify-center">v{version ?? "0"}</div>
  );
};

I have a few things happening causing some errors.

1. In VSCode on the line with const data: VersionApi = Promise.resolve(response);
I am presented with the following error:
Property 'version' is missing in type 'Promise<VersionApi>' but required in type 'VersionApi'. on data

2. When I attempt to load my page, I get the following error:
Error: No QueryClient set, use QueryClientProvider to set one
This is pointing to
  26 |
  27 | export const VersionCard = () => {
> 28 |   const { data: version, isLoading } = useQuery({
     |                                                 ^
  29 |     queryFn: fetchVersion,
  30 |     queryKey: [],
  31 |   });


3. On this line: <div className="flex items-center justify-center">v{version ?? "0"}</div>
VSCode is throwing an error of: Type 'string | VersionApi | Error' is not assignable to type 'ReactNode'. Type 'VersionApi' is not assignable to type 'ReactNode'. for {version ?? "0"}
http://localhost:3000/api/version returns the following JSON
{"version":"0.1.0"}
manged to figure it out 😄
Glad you figure it out!

I'm responding anyway for other folks to see a response if they have a similar problem 🙂

1. You don't need to wrap your version in a Promise.resolve. This is because async / await provides you a promise by design

2.

const { data: version, isLoading } = useQuery({
    queryFn: fetchVersion,
    queryKey: [],
  });


I highly recommend you to provide a queryKey, this is what react-query is using to cache your data.
It will be used to dedup identical keys (to only have one fetch at a time) and will be helpful to clear up and updates queries within the same group.

3. Type 'string | VersionApi | Error' is not assignable to type 'ReactNode' is because you are giving React an object, this not supported. Fixes is to use version.version with what I'm seeing
i added a queryKey, but it threw an error
wait. nope.. i added one and it is good
Common paper wasp
I just came here to say hello fellow FFXIV adventurer. Hope you figure it out and grace us with the BLU spell tracker
you mean BLU mage, lol
Common paper wasp
Shhhh you saw nothing
a quick preview
Common paper wasp
Looks legit man
chances of me getting it done before DT is slim, lol. but i'm gonna try
Common paper wasp
How are you going to pull the data for the spells? Any idea related to FFXIV came back to where tf do I get the data from? But yeah would be a good use case for Suspense and Loading there I think. Lmk if you do finish this, this is a neat idea
there is an API from XIV Collect 😄
my next thing i need to figure out is saving the data for the user on the spells when they are logged in which is the main page. so the routing i need to figure out
main page will be like /spells, but this needs to also be per user and be available when not logged in