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">,): voidHand it a target object (or an accessor that returns one) and the props to apply; it wires everything up and returns nothing.
Parameters
| Parameter | Type | Description |
|---|---|---|
accessor | T | undefined | Accessor<T | undefined> | The target three.js object, an accessor that returns it, or undefined. Application waits until the value is non-undefined. |
props | object | The 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, reactivelyuseProps(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
needsUpdateon 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 pass | Example | What happens |
|---|---|---|
undefined | color={undefined} | Skipped — undefined never overwrites the current value. |
| A CSS color string or hex number | color="red", color={0xff8800} | target.color.set(value). |
| An array | position={[1, 0, 0]} | target.position.fromArray(value) (or .set(...value) when the field has no fromArray). |
A single number, on a field with setScalar | scale={2} | target.scale.setScalar(2) — the same value on every axis. |
| An instance matching the field's type | quaternion={new Quaternion()} | Copied in place with .copy(value). |
| An event handler | onClick={…} | Registered on the event system (Object3D instances only). |
| Anything else | visible={false}, castShadow | Assigned 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 type | Attaches to |
|---|---|
Material | parent.material |
BufferGeometry | parent.geometry |
Fog | parent.scene.fog |
Object3D | parent.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 atparent.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:
encoding→colorSpaceoutputEncoding→outputColorSpace- the value
sRGBEncodingmaps 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
- Props and children — the tutorial walk-through of the same value conversion.
<T.*>/createTand<Entity/>— the components built onuseProps.
Last updated: 6/8/26, 11:20 AM