Skip to main content

useProps reactively applies a record of props to a three.js object, keeping it in sync as those props change. Applying a prop covers everything solid-three does with it: converting the value into the shape three expects, resolving dashed paths into nested setters, registering event listeners, attaching children, and disposing on cleanup. It is what powers every <T.*> and <Entity/> component.

Call it directly when you wrap your own three.js object — custom controls, a third-party library, an ad-hoc instance.

Signature

function useProps<T extends Record<string, any>>(
accessor: T | undefined | Accessor<T | undefined>,
props: any,
context?: Pick<Context, "requestRender" | "gl" | "props">,
): void

Hand it a target object (or an accessor that returns one) and the props to apply; it wires everything up and returns nothing.

Parameters

ParameterTypeDescription
accessorT | undefined | Accessor<T | undefined>The target three.js object, an accessor that returns it, or undefined. Application waits until the value is non-undefined.
propsobjectThe props to apply — including ref, args, attach, children, and any three.js properties.
context (optional)Pick<Context, "requestRender" | "gl" | "props">The context slice useProps reads from. Defaults to useThree(). Pass it explicitly when calling outside a <Canvas> — in tests, or a custom renderer host.

Usage

const mesh = new Mesh(new BoxGeometry(), new MeshBasicMaterial())
// Apply solid-three props to the mesh, reactively
useProps(mesh, props)

A typical wrapper instantiates the object (often in a createMemo, so it rebuilds when args change), hands the accessor to useProps, and returns null:

export function OrbitControls(props: S3.Props<typeof ThreeOrbitControls>) {
const three = useThree()
const controls = createMemo<ThreeOrbitControls>(() => {
const next = autodispose(new ThreeOrbitControls(three.camera))
next.connect(three.gl.domElement)
return next
})
useFrame(() => controls().update())
useProps(controls, props)
return null
}

Behavior

The wrapper function runs once, at mount. Each prop is then tracked independently: when a signal a prop reads changes, only that prop updates — its siblings are untouched, and the object is mutated in place rather than rebuilt.

After applying a prop, solid-three:

  • flags needsUpdate on any material or geometry that has it, so shaders recompile when they need to;
  • requests a render on the next frame when the renderer is on frameloop="demand".

The one exception is args, which rebuilds the object instead of mutating it.

How values are converted

Each prop is matched against the cases below, in order; the first that fits wins. The point is that you pass the most natural value and solid-three translates it into the call three expects.

You passExampleWhat happens
undefinedcolor={undefined}Skipped — undefined never overwrites the current value.
A CSS color string or hex numbercolor="red", color={0xff8800}target.color.set(value).
An arrayposition={[1, 0, 0]}target.position.fromArray(value) (or .set(...value) when the field has no fromArray).
A single number, on a field with setScalarscale={2}target.scale.setScalar(2) — the same value on every axis.
An instance matching the field's typequaternion={new Quaternion()}Copied in place with .copy(value).
An event handleronClick={…}Registered on the event system (Object3D instances only).
Anything elsevisible={false}, castShadowAssigned directly: target[key] = value.

In practice, one element mixes several of these:

<T.Mesh
position={[1, 0, 0]} // array → Vector3.set(1, 0, 0)
scale={2} // number → scale.setScalar(2)
rotation={[Math.PI / 4, 0, 0]} // array → Euler.set(x, y, z)
>
<T.MeshStandardMaterial
color="cornflowerblue" // string → Color.set("cornflowerblue")
emissive={[1, 0.4, 0.2]} // array → Color.set(1, 0.4, 0.2)
specular={0xff8800} // hex → Color.set(0xff8800)
/>
</T.Mesh>

Dashed paths

A hyphenated key reaches into a nested object and applies the same conversion at the leaf:

<T.DirectionalLight
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
shadow-bias={-0.0001}
/>

reads as light.shadow.mapSize.width = 1024, light.shadow.mapSize.height = 1024, light.shadow.bias = -0.0001. Each leaf is converted like any other value — position-x={1} lands on a number, material-color="blue" goes through Color.set.

When both a parent and a child of the same path are present — position={[0, 0, 0]} and position-x={2} — the child is applied last and wins.

args

args is spread into the object's constructor: new BoxGeometry(...args). It is not a runtime property — it decides how the object is built. Changing it therefore rebuilds the object: the old one is disposed and a new one mounts in its place. This is what you want for constructor-only parameters like geometry dimensions.

const [size, setSize] = createSignal(1)
<T.Mesh>
{/* Changing size rebuilds the geometry */}
<T.BoxGeometry args={[size(), size(), size()]} />
<T.MeshStandardMaterial color="cornflowerblue" />
</T.Mesh>

For anything with a runtime setter, use a normal prop — it mutates in place instead of rebuilding.

attach

By default solid-three decides where a child belongs:

Child typeAttaches to
Materialparent.material
BufferGeometryparent.geometry
Fogparent.scene.fog
Object3Dparent.children

Set attach explicitly when the default isn't what you want — for example, routing textures into a material's map slots:

<T.MeshStandardMaterial>
<Resource loader={TextureLoader} url="diffuse.jpg" attach="map" />
<Resource loader={TextureLoader} url="normal.jpg" attach="normalMap" />
</T.MeshStandardMaterial>

attach accepts:

  • a string — dashed paths work as they do for props (attach="material-emissiveMap" lands at parent.material.emissiveMap);
  • a function(parent, child) => cleanup, for attachments that aren't a simple property assignment. The returned cleanup runs on unmount.

ref

Both function and object refs work:

let mesh: Mesh | undefined
<T.Mesh ref={mesh} />
// or
<T.Mesh ref={instance => doSomethingWith(instance)} />

A function ref runs inside a render effect, so it re-runs if the underlying object's identity changes — for instance after an args rebuild.

onUpdate

A special prop, not a three.js field. It fires after each pass of prop application for the object, receiving the object itself. Use it as a "props are settled" hook for setup that can't be expressed as a single prop.

Materials and textures

Three smaller behaviors apply when you work with materials and textures.

Recompiling shaders

Toggling certain material props on or off changes which shader three needs to compile, so solid-three flags needsUpdate the moment their truthiness flips:

map, envMap, bumpMap, normalMap, transparent, morphTargets, skinning, alphaTest, useVertexColors, flatShading.

(This is on top of the needsUpdate flag set after every prop, described in Behavior.)

Legacy encoding props

encoding and outputEncoding are legacy aliases, replaced in three r152. When the target has the newer colorSpace / outputColorSpace field, solid-three rewrites them for you:

  • encodingcolorSpace
  • outputEncodingoutputColorSpace
  • the value sRGBEncoding maps to "srgb"; anything else maps to "srgb-linear".

Texture color space

When you assign a Texture whose format three uses for color data (RGBAFormat / UnsignedByteType), solid-three sets its colorSpace to match the renderer's outputColorSpace. This tracks the canvas-level linear and flat props, so flipping either updates assigned textures automatically.

See also

Last updated: 6/8/26, 11:20 AM

solid threeA SolidJS renderer for three.js — learn by reading.
Community
github