v10: expose newly loaded textures to later creator hooks in the same render

Author: DennisSmolekCreated Sep 1, 2026Updated Sep 1, 2026
Labelsbugcorev10loadingWebGPU

Description

useTexture loads textures and enrolls them in the root texture registry by default, while useLocalNodes/other creator hooks expose that registry through CreatorState.textures.

However, newly loaded textures are not visible to a creator hook later in the same component render. useTexture performs registry enrollment in a passive effect, while useLocalNodes executes its creator during render.

This prevents the direct composition the creator state appears designed to support:

typescript
const TEXTURES = {
  day: '/earth-day.jpg',
  night: '/earth-night.jpg',
}

function Globe() {
  useTexture(TEXTURES)

  const nodes = useLocalNodes(({ textures }) => {
    // CreatorState.textures is a URL-keyed Map, so .get() is intentional here.
    const day = textures.get(TEXTURES.day)
    const night = textures.get(TEXTURES.night)

    // Both are undefined on the first render after a fresh load because registry
    // enrollment has not reached useTexture's effect yet.
    return {
      dayNode: texture(day),
      nightNode: texture(night),
    }
  })

  // ...
}

This is separate from record aliases: CreatorState.textures is correctly a Map<string, Texture> keyed by URL, so textures.day is not expected to work. The issue is that even textures.get(TEXTURES.day) cannot see the texture during that initial creator run.

Current workaround

Keep and close over the direct useTexture result:

typescript
const textures = useTexture(TEXTURES)

const nodes = useLocalNodes(() => ({
  dayNode: texture(textures.day),
  nightNode: texture(textures.night),
}))

This works and the same texture objects are registered afterward, but it bypasses CreatorState.textures and requires every downstream creator to receive or close over the loader result.

Cause

The current sequence is:

  1. useTexture resumes after Suspense with loaded textures.
  2. A later useLocalNodes creator runs during that render and reads the existing registry Map.
  3. Commit completes.
  4. useTexture's effect adds the loaded textures to the registry and updates refcounts.
  5. Registry subscribers render again.

Creator hooks already have staged-resource support so resources created earlier in a render can be visible to later creators without writing Zustand state during render. useTexture enrollment currently does not participate in that staging path.

Expected behavior

When useTexture appears before a creator hook in tree/hook order, its loaded textures should be visible through CreatorState.textures during that same render:

typescript
useTexture(TEXTURES)

useLocalNodes(({ textures }) => ({
  dayNode: texture(textures.get(TEXTURES.day)),
}))

No intermediate missing-texture graph or second render should be required.

Suggested direction

Stage URL → Texture registrations during render, using the same pending-resource mechanism that makes earlier useUniforms/useNodes registrations visible to later creators. Commit the staged entries and refcount changes through the existing lifecycle-safe effect/commit path.

An equivalent CreatorState overlay would also work, provided it:

  • does not call store setState during render;
  • preserves existing URL-keyed registry semantics;
  • preserves refcounting and explicit disposal behavior;
  • remains StrictMode and aborted-render safe.

Acceptance criteria

  • A fresh single-URL useTexture result is visible to a later creator in the same render.
  • Record and array inputs stage every URL correctly.
  • Cached textures retain their existing identity and modifications.
  • No set-state-during-render warnings or update loops.
  • Aborted renders do not permanently enroll or retain textures.
  • Commit/unmount refcounts remain balanced under StrictMode.

Related: #3849 fixed record/array registry effect churn; this issue concerns visibility before that effect runs.

Source: pmndrs/react-three-fiber