To understand a technology, one must first understand what it represents so that it is easier to understand the ideas behind it.
In wikipedia’s definition
Suspense is a state of mental uncertainty, anxiety, of being undecided, or of being doubtful. Suspense is a state of mental uncertainty, anxiety, uncertainty, or doubt.
Suspense is a component that suspects components in React, so it might be a bit abstract. Let’s take it a step further.
Suspense is still experimental features and is not recommended for use in development environments.
Suspense lets your components “wait” for something before they can render.
Suspense is officially the simplest way to describe it. Suspense allows us to render things before components can finish rendering, but it feels like why not call it “Loader” because it currently feels like loading?
In most cases, the fallback parameters accepted in Suspense look like they’re made for rendering things like
Suspense is not a data fetching library. It’s a mechanism for data fetching libraries to communicate to React that the data a component is reading is not ready yet.
Suspense is, by definition, too bad to use the name if it’s just for data capture. The React team actually tends to use Suspense as a function of interworking with components that contain Promise states and have additional features such as parallelization.
Before trying out Suspense let’s take a look at code examples from the authorities to compare the differences between using and not using Suspense.
The first common method is useEffect or componentDidMount
You define something like this to get the data
// Functional components:
useEffect(() = >{ fetchSomething(); } []);// Class component:
componentDidMount() {
fetchSomething();
}
Copy the code
We call this approach “fetch-on-render” because it doesn’t start fetching until after the component has rendered on the rasterism Screen. This leads to a problem known as a “waterfall”.
We call this method of rendering “cake-on-rende”. What it means is that the component starts to fetch data only after rendering in the view, which leads to a problem called Waterfall. If you look down, you’ll know what waterfall is.
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() = > {
fetchUser().then(u= >setUser(u)); } []);if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() = > {
fetchPosts().then(p= >setPosts(p)); } []);if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Copy the code
What happens to the view when you render these two components. First of all, the two components have different methods to fetch data, one is fetchUser and the other is fetchPosts.
Another key point is the Render section of the ProfilePage.
if (user === null) { return <p>Loading profile... </p>; } <> <h1>{user.name}</h1> <ProfileTimeline /> </>Copy the code
If (user === null) Loading is not complete. In fact, this will block the rendering of the
We’ll see a process like this happen.
- Let’s start rendering
user details
component - Start getting data
- Determine if the data is ready
- Not good -> display
Loading profile...
- Ok -> Start rendering
<ProfileTimeline />
- then
<ProfileTimeline />
And started doing similar things - To get the data
- Not good -> display
Loading posts...
- Ok -> show concrete
posts
To put this in perspective, first of all, this would not be a problem in most businesses, but the process of the
To waterfall means to describe this phenomenon: an unintentional sequence that should have been parallelized. It’s this thing that’s supposed to be parallel but turns into a meaningless serialization operation.
The second variant promise.then
Putting aside Relay and Graphql and all that stuff and combining requests together, the most generic gameplay a front-end can come up with is promise.all.
Combine the two requests mentioned earlier.
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) = > {
return{user, posts}; })}Copy the code
It looks like this. So we can make sure that these two are parallel. And you can guarantee that these two will be processed after the fetch is complete.
// Kick off fetching as early as possible
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() = > {
promise.then(data= >{ setUser(data.user); setPosts(data.posts); }); } []);if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}
// The child doesn't trigger fetching anymore
function ProfileTimeline({ posts }) {
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Copy the code
Take a look at the rendering flow
- At the same time to obtain
users details
和posts
- Waiting for the
- Neither of them is good – both show loading
- Both of them are ready. – Both of them are on display
Yes, the waterfall problem is gone, but it brings a new problem, the two must be good at the same time, then the view will be updated, that is, if either one has not been acquired, or failed, the rendering will not come out, always processing loading state, and exceptions are quite difficult to handle so whole.
This is actually good enough for the group to handle many scenarios, but as the code increases, the interface increases, and so on, it becomes a hassle to maintain this thing, and as the component hierarchy increases, more states need to be passed, and more concepts and things are introduced.
Some people might say we don’t have to use promise.all, which is fine, because we just need to pull things to the top and useEffect to update them. This is a solution, but it’s not good enough, so we don’t have to worry too much about it.
useEffect(() = > {
fetchUser().then(user= > setUser(user));
fetchPosts().then(posts= > setPosts(posts));
});
Copy the code
Suspense
Previously, we controlled data acquisition by state. SetState, useState, things like that.
From now on, turn your ideas upside down. Forget about it and think of the process of getting data as a state, or we only care about the result of getting data and don’t want to do the crap of loading and other in-between states.
Suspense, we don’t wait for the response to come back before we start rendering. In fact, we start rendering pretty much immediately after kicking off the network request.
In Suspense we will render immediately as soon as we connect to the web. This is a bit of a cheat, as there is no control statement like === null in the sample code. So let’s look at the example that I just showed you.
/ / pseudo code
function fetchProfileData() {
let userPromise = fetchUser();
let postsPromise = fetchPosts();
return {
user: wrapPromise(userPromise),
posts: wrapPromise(postsPromise)
};
}
// This is not a Promise. It's a special object from our Suspense integration.
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>} ><ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>} ><ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Copy the code
This is not a Promise. It’s a special object from our Suspense integration.
Pay attention to the first sentence, which is crucial. Suspense though it’s related to Promise. It doesn’t use Promise directly though, but instead requires users to write a specific format to use Suspense. The format looks something like this.
- Pending (for promise pending) ->Put (throw)a
promise
object - Error (this is a big promise) — > Returns an error
- Success (对 promise rejected) -> Return a result
The function that you end up defining looks something like this.
function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
(r) = > {
status = "success";
result = r;
},
(e) = > {
status = "error"; result = e; });return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
returnresult; }}}; }Copy the code
Don’t worry about how react uses the data structure and suspense components, but it’s a simple function package that can implement it:
- There is no
waterfall
The code of - Maintainable, readable code
This is a major breakthrough. I’m going to write another article about implementation principles and things like that, and it’s going to be a little bit longer. In addition, this is just a simple code example, but there are more complex concurrent race situations that have not been covered, but will be covered in another article.
This Demo is official and you can click on it to see what it looks like.
Suspense has an outer layer and a small one that wraps around
Or to understand how it’s rendered
- I called it at the very beginning
fetchProfileData
In order to getuser
和post
Two dates. - The React totryApply colours to a drawing
ProfilePage
This component, and then it finds two things in itProfileDetails
andProfileTimeline
. - Then React startstryApply colours to a drawing
ProfileDetails
, start callingresource.user.read()
Since it was just launched, there is nothing, the component is suspended (suspends
). React threw it aside and started rendering other stuff likeProfileTimeline
. - The React againtryApply colours to a drawing
ProfileTimeline
, found it withProfileDetails
Almost nothing, also hang it (suspends
). - OK, that’s it, because none of them are good, so it’s going to start showing
fallback
The component. - Over time,
resource.user
I got something. The outermost onefallback
It will be removed, and it won’t be displayed<h1>Loading profile... </h1>
This component. But it still shows<h1>Loading posts... </h1>
untilposts
After data acquisition is completed. - And finally all
fallback
It’s all gone.
Additional gameplay
Suspense /> isn’t really one-to-one with components; it can be one-to-many, as in this case
<Suspense fallback={<h1>Loading Content... </h1>}> <ProfileDetails /> <ProfileTimeline /> </Suspense>Copy the code
It works, but I made it a little longer, so I can try it out, which means we can play a lot more.
Hypothetical realization
Suspense can be inferred from this phenomenon that Suspense collects child component promises, but each of them is a scope. Suspense does not collect promises for wrapped components if child components are wrapped in Suspense. It’s also safe to assume that Suspense will only collect first-level promises, but collecting even a subset of them is a bit anti-human and a performance drag.
Since suspenser is thrown as an exception, we also need to use componentDidCatch to catch suspenser. And relies on componentDidCatch to display the corresponding Fallback component.
Unfortunately, THE ‘react’ method didn’t work, so I changed it and couldn’t get the ‘then’ method. Throw {suspender}. That’s ok.
read() { if (status === "pending") { throw { suspender }; } else if (status === "error") { throw result; } else if (status === "success") { return result; }}Copy the code
Nothing else has changed. Suspense version I implemented myself. Although the console has a bunch of bugs, ignore them for now.
Easy version link. There was an error on the way in:
Error
[object Object]
Copy the code
Click the X on the right to turn it off and you’ll see what happens.
The original