[v10] Add explicit dependency-array semantics to useLocalNodes

Author: DennisSmolekCreated Sep 16, 2026Updated Sep 16, 2026
Labelsenhancementv10WebGPU

Summary

Implementation follow-up to #3888, part 1. Preserve clean inline creators while adding an explicit dependency-array contract for useLocalNodes. The separate resource-subscription work addresses part 2 of that report. This issue does not replace its reproduction or measured results.

Verified on remote v10 at 979dc2d2e39cf0f8787714a81f47ff19ade384fb: useLocalNodes in packages/fiber/src/webgpu/hooks/useNodes.tsx memoizes against [store, creator, uniforms, nodes, textures, hmrVersion]. Inline function identity changes on each component render, so the creator reruns even when construction inputs are unchanged.

Proposed public contract

typescript
function useLocalNodes<T extends Record<string, unknown>>(
  creator: LocalNodeCreator<T>,
  deps?: React.DependencyList,
): T

The presence of the array selects explicit dependency behavior; do not test array length or truthiness of its members.

Call Component-render behavior
useLocalNodes(creator) Re-evaluate on every component render; captured values are current.
useLocalNodes(creator, []) Reuse the local result across ordinary component renders. No declared captured-value dependencies.
useLocalNodes(creator, [strength]) Reuse until a declared dependency changes by Object.is.

In explicit-dependency mode, relevant registered resource replacements, a change of owning store, and deliberate HMR/manual invalidation remain independent rebuild triggers. [] does not mean never rebuild or never remount. The separate subscription issue narrows which registry changes count as relevant.

typescript
// Resource-driven composition: no surrounding JS inputs.
const local = useLocalNodes(({ uniforms }) => ({
  result: uniforms.strength.mul(2),
}), [])

// strength is a JavaScript construction input captured from this render.
const local = useLocalNodes(({ nodes }) => ({
  result: nodes.noise.mul(strength),
}), [strength])

Why this design

The convenience is access to the rendering context, scoped shared resources, staged registrations, and rebuild integration while keeping compositions local. Callers should not need an extra useCallback just to use the documented inline syntax efficiently.

A runtime hook cannot infer arbitrary JavaScript closure dependencies. Merely putting the creator in a ref and deleting it from the memo dependencies silently makes captured inputs stale. An explicit array makes the construction dependencies reviewable.

When a declared dependency, relevant resource, or HMR invalidation triggers evaluation, use the creator belonging to the current render. An omitted dependency does not itself trigger evaluation, but its latest captured value can be observed on a later legitimate rebuild; this matches the usual memo model and is not a promise to freeze the initial closure forever.

React versus Three/TSL responsibilities

Track graph construction inputs and shared resource identity. Do not reconstruct a graph for every .value mutation of an existing UniformNode or for updates handled by Three's camera/time/reference nodes. Reading a primitive JS value while constructing a graph is different from supplying a live node/reference. Access to RootState does not imply recursive subscriptions to every mutable Three property.

Creator execution remains render-phase calculation. This proposal adds no effect/cleanup execution and no store publication during render. Keep original Three objects and exact return-type inference.

Compatibility and API interactions

  • Inline no-array calls already rerun on each component render. Explicitly making all no-array calls do so changes behavior for callers passing a stable useCallback creator; document this and migrate those callers to an explicit array. Do not describe the change as entirely behavior-preserving.
  • Keep hooks in a consistent order regardless of whether an array is supplied. Define fixed array length and stable mode as caller requirements, and provide development diagnostics where practical rather than forwarding a changing-length conditional dependency list to React.
  • #3890 proposes a cleanup callback as argument two; #3893 concerns effect-only creators. Reconcile the signature before landing. Reserve the second argument for dependencies in this proposal; any effect API needs a separate lifecycle-safe design.
  • Explicit arrays introduce stale-closure risk if users omit captured construction inputs. Document it and investigate custom exhaustive-dependency lint coverage; do not assume generic additional-effect-hook settings automatically support this value-returning API correctly.
  • Neither this API nor React memoization guarantees exactly one creator invocation under StrictMode, initial suspension, or discarded renders.

Implementation guidance

Separate the reasons to evaluate: omitted deps, changed explicit deps, relevant resource identities, owning-store changes, and rebuild/HMR generation. Do not use creator identity as an invalidation reason when explicit deps are supplied. Do not use deep equality or function source serialization to infer closure equivalence. Retain staged-resource visibility from createLazyCreatorState and avoid leaking creator state from abandoned renders.

The subscription improvement can land separately: until it does, explicitly document that whole-map updates remain broader than the final desired contract.

Acceptance criteria

  • Inline callback + [] preserves the local node/result identities over unrelated parent and local-state renders.
  • [strength] reconstructs when strength changes and reuses the result when it does not.
  • No array reevaluates on each component render, including a stable callback argument.
  • A relevant resource replacement and store change reconstruct using the current render's creator.
  • Updating an existing uniform's value does not trigger a registry-based graph rebuild.
  • HMR/manual invalidation still reconstructs explicit-array compositions.
  • StrictMode, initial Suspense, interrupted renders, and unmount/remount do not publish invalid state or rely on exactly-once execution.
  • Types preserve exact creator return keys/types and accept readonly dependency lists.
  • Tests distinguish creator execution, node identity, material remount, and actual GPU recompilation; do not equate those metrics.
  • Update canonical TSL docs, migration notes, source examples, and API declaration exports; explicitly explain no-array versus [] and live TSL values versus JS construction inputs.

Sources: https://react.dev/reference/react/useMemo and the pinned v10 implementation above.

Companion implementation issue: #3919 (automatic resource subscription narrowing).

Source: pmndrs/react-three-fiber