threlte logo
Advanced

Custom Abstractions

A lot of the components you will find in the package @threlte/extras are abstractions on top of the <T> component. These abstractions provide extra functionality like automatically invalidating the frame or providing default values or extra props.

A common use case for custom abstractions is to create a component that is a fixed entity in your Threlte app which you want to reuse in multiple places or which exposes a specific behavior. As an example, let’s create a component that is made up from multiple <T> components resembling a tile of some kind:

Tile.svelte
<script>
  import { T } from '@threlte/threlte'
  import { MathUtils } from 'three'

  let { children } = $props()
</script>

<T.Group>
  <!-- 2x2 Tile -->
  <T.Mesh rotation.x={-90 * MathUtils.DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  {@render children()}
</T.Group>

Let’s see what implementing that component looks like:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<Tile />

Props

The <Tile> component is now available in the scene and can be reused as many times as you want.

Now we’d like to assign a different position to every <Tile> in order to position it in the scene. We can do that by passing a position prop to the <Tile> component:

Scene.svelte
<script>
  import Tile from './Tile.svelte'
</script>

<Tile position={[0, 0, 0]} />
<Tile position={[2, 0, 0]} />
<Tile position={[4, 0, 0]} />

That doesn’t work yet. The component <Tile> internally needs to make use of the position prop to set the position of its children. We can do that by spreading props on the <T.Group> component at the root hierarchy of <Tile>:

Tile.svelte
<script>
  import { T } from '@threlte/threlte'
  import { MathUtils } from 'three'

  let { children, ...props } = $props()
</script>

<T.Group {...props}>
  <!-- 2x2 Floor -->
  <T.Mesh rotation.x={-90 * MathUtils.DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  {@render children()}
</T.Group>

Types

The following section assumes you use TypeScript.

The last thing we need to do is to add types to our custom abstraction so that IDEs like VS Code can provide autocompletion and type checking. We will create a types.ts file next to the Tile.svelte file and add the following content:

types.ts
import type { Props } from '@threlte/core'
import type { Group } from 'three'

export type TileProps = Props<Group>

As of now it’s necessary to declare the props in a separate file. If you declare them inside the <script> block, the Svelte compiler flattens the resulting type, removing JSDoc comments and possibly breaking it.

The generic type Props<Type> is a utility type that extracts all possible props that a <T> component accepts – in this case <T.Group>. It also creates a bindable ref prop that can be used to bind to the group created by the <T.Group> component.

We need to import the TileProps type in our Tile.svelte file and make use of the ref prop. For that we:

  • Add the lang="ts" attribute to the <script> block
  • Import the TileProps type
  • Add the bindable prop ref to the props
  • Bind ref to the bindable prop ref of the root <T.Group> component
  • Pass ref to the children() block
Tile.svelte
<script lang="ts">
  import { T } from '@threlte/threlte'
  import { MathUtils } from 'three'
  import type { TileProps } from './types'

  let { children, ref = $bindable(), ...props }: TileProps = $props()
</script>

<T.Group
  {...props}
  bind:ref
>
  <!-- 2x2 Floor -->
  <T.Mesh rotation.x={-90 * MathUtils.DEG2RAD}>
    <T.PlaneGeometry args={[2, 2]} />
    <T.MeshStandardMaterial />
  </T.Mesh>

  {@render children({ ref })}
</T.Group>

Now we can use the <Tile> component in our scene and get autocompletion and type checking.

Scene.svelte
<script>
  import Tile from './Tile.svelte'
  import { TransformControls } from '@threlte/extras'
</script>

<Tile position={[2, 0, 2]}>
  {#snippet children({ ref })}
    <TransformControls object={ref} />
  {/snippet}
</Tile>