threlte logo
@threlte/extras

<Suspense>

The component <Suspense> allows you to orchestrate the loading of resources inside (nested) child components. The implementation roughly follows a subset of the concept established by the React Suspense API and has certain limitations.

The idea is to allow a parent component to make decisions based on the loading state of child components. The parent component can then decide to show a loading indicator or a fallback component while the child component is loading.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
    background-color: rgb(47 125 198 / 0.2);
  }
</style>
<script lang="ts">
  import { T, useThrelte } from '@threlte/core'
  import { Suspense, Text } from '@threlte/extras'
  import { Color } from 'three'
  import Spaceship from './Spaceship.svelte'
  import StarsEmitter from './StarsEmitter.svelte'

  const { size, scene } = useThrelte()

  scene.background = new Color('black')

  let zoom = $size.width / 50
  $: zoom = $size.width / 50
</script>

<T.OrthographicCamera
  position={[-40, 25, 40]}
  makeDefault
  {zoom}
  oncreate={(ref) => {
    ref.lookAt(0, 0, -8)
  }}
/>

<T.DirectionalLight
  position={[5, 10, 3]}
  intensity={2.5}
/>

<Suspense final>
  {#snippet fallback()}
    <Text
      position.z={-8}
      text="Loading..."
      fontSize={1}
      color="white"
      anchorX="50%"
      anchorY="50%"
      oncreate={(ref) => {
        ref.lookAt(-40, 25, 40)
      }}
    />
  {/snippet}

  {#snippet error({ errors })}
    <Text
      position.z={-8}
      text={errors.map((e) => e).join(', ')}
      fontSize={1}
      color="white"
      anchorX="50%"
      anchorY="50%"
      oncreate={(ref) => {
        ref.lookAt(-40, 25, 40)
      }}
    />
  {/snippet}

  <StarsEmitter />

  <Spaceship
    name="Bob"
    position={[-12, 0, 3]}
  />
  <Spaceship
    name="Challenger"
    position={[10, 5, 6]}
  />
  <Spaceship
    name="Dispatcher"
    position={[8, 3, -23]}
  />
  <Spaceship
    name="Executioner"
    position={[12, -4, 6]}
  />
  <Spaceship
    name="Imperial"
    position={[-1, 0, -21]}
  />
  <Spaceship
    name="Insurgent"
    position={[-13, 1, -21]}
  />
  <Spaceship
    name="Omen"
    position={[-9, -5, 13]}
  />
  <Spaceship
    name="Pancake"
    position={[-9, -3, -9]}
  />
  <Spaceship
    name="Spitfire"
    position={[1, 0, 1]}
  />
  <Spaceship
    name="Striker"
    position={[8, -1, -10]}
  />
  <Spaceship
    name="Zenith"
    position={[-1, 0, 13]}
  />
</Suspense>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Float, useGltf, useSuspense } from '@threlte/extras'
  import type { SpaceshipProps } from './Spaceship.svelte'

  type $$Props = SpaceshipProps

  export let name: $$Props['name']

  const suspend = useSuspense()

  const gltf = suspend(useGltf(`/models/spaceships/${name}.gltf`))
</script>

{#await gltf then { scene }}
  <T.Group {...$$restProps}>
    <Float
      floatIntensity={3}
      speed={3}
    >
      <T is={scene} />
    </Float>
  </T.Group>
{/await}
import type { Props } from '@threlte/core'
import type { SvelteComponent } from 'svelte'
import type { Group } from 'three'

export type SpaceshipProps = Props<Group> & {
  name:
    | 'Bob'
    | 'Challenger'
    | 'Dispatcher'
    | 'Executioner'
    | 'Imperial'
    | 'Insurgent'
    | 'Omen'
    | 'Pancake'
    | 'Spitfire'
    | 'Striker'
    | 'Zenith'
}

export default class Spaceship extends SvelteComponent<SpaceshipProps> {}
<script lang="ts">
  import { useTask } from '@threlte/core'

  // your script goes here

  import { Instance } from '@threlte/extras'
  import { Color } from 'three'

  let positionX = Math.random() * 100 - 50
  let positionY = Math.random() * 100 - 50
  let positionZ = 100

  const colors = ['#FFF09E', '#B8DFFF', '#CADBFF', '#FFEBBE']

  // random element from array
  const color = new Color(colors[Math.floor(Math.random() * colors.length)])

  useTask((delta) => {
    const f = 1 / 60 / delta
    positionZ -= 10 * f
  })
</script>

<Instance
  {color}
  position.x={positionX}
  position.y={positionY}
  position.z={positionZ}
/>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { InstancedMesh } from '@threlte/extras'
  import Star from './Star.svelte'

  type Star = {
    id: string
    spawnedAtFrame: number
  }

  const makeUniqueId = () => {
    return Math.random().toString(36).substring(2, 9)
  }

  let stars: Star[] = []
  /**
   * spawnRate is the number of stars to spawn per frame
   */
  const spawnRate = 2
  const lifetimeInFrames = 40

  let frame = 0
  useTask(() => {
    frame += 1
    for (let i = 0; i < spawnRate; i++) {
      stars.push({
        id: makeUniqueId(),
        spawnedAtFrame: frame
      })
    }
    stars.forEach((star) => {
      if (frame - star.spawnedAtFrame > lifetimeInFrames) {
        // remove star from array
        stars.splice(stars.indexOf(star), 1)
      }
    })
    stars = stars
  })
</script>

<InstancedMesh>
  <T.BoxGeometry args={[0.04, 0.04, 30]} />
  <T.MeshBasicMaterial
    color="white"
    transparent
    opacity={0.2}
  />

  {#each stars as star (star.id)}
    <Star />
  {/each}
</InstancedMesh>

Usage

In a child component

Let’s have a look at a simple component that loads a model with the hook useGltf.

Model.svelte
<script>
  import { T } from '@threlte/core'
  import { useGltf } from '@threlte/extras'

  const gltf = useGltf('model.gltf')
</script>

{#await gltf then { scene }}
  <T is={scene}>
{/await}

We can make that component suspense-ready by using the hook useSuspense and passing the promise returned by useGltf to it.

Model.svelte
<script>
  import { T } from '@threlte/core'
  import { useGltf, useSuspense } from '@threlte/extras'

  const suspend = useSuspense()
  const gltf = suspend(useGltf('model.gltf'))
</script>

{#await gltf then { scene }}
  <T is={scene}>
{/await}

Suspense-ready

Suspense-ready means it has the ability to hit a <Suspense> boundary if there is any. If there’s no <Suspense> boundary, the component will behave as usual.

In a parent component

Now we implement the component “Model.svelte” in a parent component:

Parent.svelte
<script>
  import Model from './Model.svelte'
</script>

<Model />

To let the parent component decide what to do while the model component is loading, we wrap the child component in a <Suspense> component and show a fallback component while the child component is loading.

Parent.svelte
<script>
  import Model from './Model.svelte'
  import Fallback from './Fallback.svelte'
  import { Suspense } from '@threlte/extras'
</script>

<Suspense>
  <Model />

  {#snippet fallback()}
    <Fallback />
  {/snippet}
</Suspense>

Error Boundary

In contrast to the React Suspense API, the <Suspense> component also acts as an error boundary for async requests made in child components wrapped in suspend.

The "error" snippet will be mounted as soon as an error is thrown in a child component wrapped in suspend and the snippet prop errors can be used to display a meaningful error message.

UX considerations

The <Suspense> component is a great tool to improve the user experience of your application. However, it is important to consider the following points:

  1. The <Suspense> component will only ever show one of the available slots at any time:
  • The snippet "error" will be shown as soon as an error is thrown.
  • The snippet "fallback" will be shown as long as a child component is suspended.
  • The default slot will be shown as long as no child component is suspended and no error has been thrown.
  1. Umounting a component that suspends rendering through awaiting a resource or throwing an error will discard its suspend state.
  2. Directly citing the React Suspense API:

    Don’t put a Suspense boundary around every component. Suspense boundaries should not be more granular than the loading sequence that you want the user to experience.

  3. A <Suspense> component can be re-suspended if a child component invokes suspend again. The property final on the component <Suspense> can be used to prevent this behavior.

Limitations

  1. It’s important to keep in mind that while the contents of the default slot are not visible, they are still mounted and the component is otherwise fully functional. This means that any side effects (for instance useTask hooks) that are invoked in a suspense-ready child component will be executed immediately.
Model.svelte
<script>
  import { T, useTask } from '@threlte/core'
  import { useGltf, useSuspense } from '@threlte/extras'

  const suspend = useSuspense()
  const gltf = suspend(useGltf('model.gltf'))

  useTask(() => {
    // This will be executed immediately
  })
</script>

{#await gltf then { scene }}
  <T is={scene}>
{/await}
  1. Also, in contrast to the React Suspense API, the <Suspense> component does not support showing a stale version of a child component while it is loading.

Component Signature

Props

name
type
required
default
description

final
boolean
no
false
If final is set to true, components cannot re-suspend the suspended state.

Events

name
payload
description

load
void
Fires when all child components wrapped in `suspend` have finished loading.

suspend
void
Fires when a child component suspends.

error
Error
Fires when an error is thrown in a child component wrapped in `suspend`.