Перейти к содержанию

Suspense

import { Aside } from "~/components/configurable/Aside";

{"<suspense>"}

A component that tracks all resources read under it and shows a fallback placeholder state until they are resolved. What makes Suspense different than Show is it is non-blocking in that both branches exist at the same time even if not currently in the DOM. This means that the fallback can be rendered while the children are loading. This is useful for loading states and other asynchronous operations.

1
2
3
4
function Suspense(props: {
  fallback?: JSX.Element;
  children: JSX.Element;
}): JSX.Element;

Here's an example of a Suspense component that shows a loading spinner while the User component is loading.

1
2
3
<Suspense fallback={<LoadingSpinner />}>
  <AsyncComponent />
</Suspense>

Nested Suspense

{""} is triggered whenever a resource is read under the suspense boundary, and waits until all resources read under the suspense boundary have resolved. Often, however, you may not want this behavior. For example, if your entire page is wrapped in suspense, you may not want a resource that only populates a certain part of the page to trigger suspense. In that case, you can wrap that resource usage its own suspense boundary, and the resource will only trigger the closest suspense boundary.

For example, in the code below, only the title() resource will trigger the top level suspense boundary, and only the data() resouce will trigger the nested suspense boundary:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const MyComponent = () => {
    const [title] = createResource(async () => { /* fetcher code here */ })
    const [data] = createResource(async () => { /* fetcher code here */ })
    <Suspense>
        <h1>{title()}</h1>
        <Suspense>
            <DataComponent>{data()}</DataComponent>
        </Suspense>
    </Suspense>
}

Understanding the purpose of {""} in Solid:

To understand the purpose of suspense, let's consider the following code snippets. These snippets will have some drawbacks which we will solve by using suspense. We will also see how it is possible to use suspense yet not reap its benefits.

Our example use case is to display a user profile. A naive snippet would look like this:

1
2
3
4
5
6
7
8
9
const MyComponentWithOptionalChaining = () => {
    const [profile] = createResource(async () => { /* fetcher code here */ })
    return (
        <>
            <div>{profile()?.name}</div>
            <div>{profile()?.email}</div>
        </>
    )
}

In this code, profile() starts as undefined, and when the fetcher code finishes, resolves to an object with name and email properties. Although the resource has not resolved yet, the two divs are already created and attached to the document body, albeit with empty text nodes. Once the resource resolves, the divs are updated with the appropriate data.

The downside of this approach is that the user is shown an empty component - let's see if we can do better than that in this next snippet:

1
2
3
4
5
6
7
8
9
const MyComponentWithShow = () => {
    const [profile] = createResource(async () => { /* fetcher code here */ })
    return (
        <Show when={profile()} fallback={<div>fetching user data</div>}>
            <div>{profile().name}</div>
            <div>{profile().email}</div>
        </Show>
    )
}

In this snippet, we first show a fallback when the resource hasn't resolved yet, and then switch to showing the profile data once it has. This results in a better user experience.

On the other hand, there is a slight downside to this approach. In our first example (using optional chaining), the divs were created immediately, and once the resource resolves all that is needed to be done is to fill in the text of the divs. But in our second example (using <Show>), the divs are only created once the resource has resolved, which means there is more work that needs to be done after the resource has resolved before the data can be shown to the user (of course, in this toy example the amount of DOM work is relatively trivial).

We can have the best of both worlds by using {""}:

1
2
3
4
5
6
7
8
9
const MyComponentWithSuspense = () => {
    const [profile] = createResource(async () => { /* fetcher code here */ })
    return (
        <Suspense fallback={<div>fetching user data</div>}>
            <div>{profile()?.name}</div>
            <div>{profile()?.email}</div>
        </Suspense>
    )
}

In this case, the divs are created immediately, but instead of being attached to the document body, the fallback is shown. Once the resource resolves, the text in the divs is updated, and then they are attached to the document (and the fallback removed).

It is important to note that execution of the component does not pause when using suspense. Instead, when a resource is read under a suspense boundary, it ensures that the nodes are not attached to the document until after the resource has resolved. Suspense allows us to have the best of both worlds: do as much work as we can before the resource resolves, and also show a fallback until then.

With this in mind, we can understand that there isn't much gained from suspense in the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const MyComponentWithSuspenseAndShow = () => {
    const [profile] = createResource(async () => { /* fetcher code here */ })
    return (
        <Suspense fallback={<div>fetching user data</div>}>
            <Show when={profile()}>
                <div>{profile().name}</div>
                <div>{profile().email}</div>
            </Show>
        </Suspense>
    )
}

In this code, we don't create any DOM nodes inside {""} before the resource resolves, so it is pretty much the same as the second example where we only used <Show>.

Props

Name Type Description
fallback JSX.Element The fallback component to render while the children are loading.

Комментарии