Support effect-only useNodes/useLocalNodes callbacks without synthetic returns
Problem
useNodes creators must return a Record<string, TSLNodeLike>, and useLocalNodes creators must return a record as well. That is useful when another consumer needs the resulting nodes, but it forces a synthetic return object when the creator installs a node directly onto Three.js state and nothing needs to consume the result.
For example, a custom scene fog graph can naturally be expressed as:
const fogValues = useControls('sprites fog', {
fogColor: '#0000ff',
near: 1500,
far: 2100,
})
useUniforms(fogValues)
useNodes(({ scene, uniforms }) => {
scene.fogNode = fog(
uniforms.fogColor,
rangeFogFactor(uniforms.near, uniforms.far),
)
})Today this fails overload resolution because the callback returns void:
Type 'void' is not assignable to type 'Record<string, TSLNodeLike>'The callback has to return an otherwise-unused object solely to satisfy the hook contract:
useNodes(({ scene, uniforms }) => {
scene.fogNode = fog(
uniforms.fogColor,
rangeFogFactor(uniforms.near, uniforms.far),
)
return { fogNode: scene.fogNode }
})The same friction applies to useLocalNodes when its result is intentionally not consumed.
Proposal
Allow useNodes and useLocalNodes to accept effect-only creators that return nothing. The hooks should still provide CreatorState, execute with their existing create/rebuild semantics, and avoid requiring a fake node record when there is no external consumer.
Possible overload shape:
useNodes(creator: (state: CreatorState) => void): void
useLocalNodes(creator: (state: CreatorState) => void): voidExisting record-returning overloads and registration behavior would remain unchanged.
Lifecycle concern
This should be more than a TypeScript-only relaxation. Both creators currently execute during render (useNodes through resource creation and useLocalNodes through useMemo). Allowing callbacks intended purely for imperative assignment raises React lifecycle questions:
- An aborted concurrent render should not leave mutations on
scene,material, or another Three object. - An effect-only callback may need cleanup when the component unmounts or resources rebuild.
- A
voiduseNodesresult has nothing to register in the node store.
If render-phase mutation is intentionally out of scope, a dedicated commit-phase API such as useNodeEffect may be safer than adding void overloads. It could receive the same CreatorState and optionally return an effect cleanup:
useNodeEffect(({ scene, uniforms }) => {
const previousFogNode = scene.fogNode
scene.fogNode = fog(uniforms.fogColor, rangeFogFactor(uniforms.near, uniforms.far))
return () => {
scene.fogNode = previousFogNode
}
})The main request is an ergonomic, lifecycle-safe path for building and installing TSL nodes directly without manufacturing a return value that no consumer uses.
Source: pmndrs/react-three-fiber