Suspense Usage and loading.tsx
Answered
Caspian Nightworth posted this in #help-forum
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).
The contents of my two files are as follows:
I am not seeing my ShadCN skeleton while this version is being fetched. Where am I going wrong in my understanding?
|
| - 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.tsximport { 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
- p3 (/p3) using
I hope this three example can help, and I'm available if you have questions 🙂
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 setupI hope this three example can help, and I'm available if you have questions 🙂
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);
}, []);@Caspian Nightworth yeah, i believe i follow. though the testing of this code didn't show like a loading ui
tsx
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);
}, []);
"though the testing of this code didn't show like a loading ui"
elaborate
elaborate
actually, do you mean there is a delay?
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);
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
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
@Caspian Nightworth i see the delay. it is still there, but i'm not seeing my loading Skeleton
can you show me your render code?
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.tsximport { 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
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
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
- p3 (/p3) using
I hope this three example can help, and I'm available if you have questions 🙂
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 setupI 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
I have a few things happening causing some errors.
1. In VSCode on the line with
I am presented with the following error:
2. When I attempt to load my page, I get the following error:
This is pointing to
3. On this line:
VSCode is throwing an error of:
appVersion.tsximport { 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.tsximport { 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 data2. When I attempt to load my page, I get the following error:
Error: No QueryClient set, use QueryClientProvider to set oneThis 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.
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.
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 seeingi 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
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