Skip to content

fix(EffectComposer): fix StrictMode composer leak, guard against r3f reorder bug - #353

Merged
kvvasuu merged 2 commits into
masterfrom
fix/03-effectcomposer-leak-and-reorder
Jul 31, 2026
Merged

fix(EffectComposer): fix StrictMode composer leak, guard against r3f reorder bug#353
kvvasuu merged 2 commits into
masterfrom
fix/03-effectcomposer-leak-and-reorder

Conversation

@kvvasuu

@kvvasuu kvvasuu commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Stacked PR 3/6 — part of a split of #350 .
← based on #352 · → followed by #354

Note: dispose coverage for ColorAverage isn't added here on purpose —
ColorAverage doesn't dispose itself until #355.

Fixes #270

EffectComposerImpl was created inside useMemo, which doesn't guarantee its cleanup runs before React discards a memoized value - this leaked the composer (and its WebGL resources) under StrictMode. It's now created and disposed inside a useState/useEffect lifecycle instead.

Pass collection walks the real r3f instance tree (group.current.__r3f .children) and rebuilds the pass list from scratch whenever it or the composer changes, so the order always matches current JSX - including through wrapper components - after a reorder or a remount.

While testing the reorder case, found a genuine bug in r3f's host config: when React moves (not remounts) an existing child - e.g. a key-preserving reorder of a keyed effect list - insertBefore/ appendChild splice the moved instance into its new slot without detaching it from the old one first, leaving a stale duplicate in Instance.children. Worth reporting upstream. Added a cheap dedupe (keep each object's last occurrence, sorted by that position) that recovers the correct order either way and is a no-op when the bug isn't present.

Also widens children from JSX.Element | JSX.Element[] to ReactNode, which was rejecting the common condition && <Effect/> idiom (that expression is false | Element, and false isn't assignable to either half of the union). The tree-walk already tolerates arbitrary children gracefully - it just filters for instanceof Effect || instanceof Pass - so the stricter type wasn't buying any actual safety.

Replaces the old root-level EffectComposer.test.tsx (superseded, predates this suite and used a different mock-root setup) with a suite covering pass registration/composition order, StrictMode disposal, and the reorder/dedupe behavior above. (ColorAverage-specific dispose coverage lands with the primitive-effect dispose fix, a few commits later - ColorAverage doesn't dispose itself until then.)

@kvvasuu
kvvasuu requested a review from krispya July 29, 2026 18:25
@kvvasuu
kvvasuu force-pushed the fix/03-effectcomposer-leak-and-reorder branch from 7df1c67 to 0d18afa Compare July 30, 2026 20:31
Base automatically changed from fix/02-wrapeffect-refactor to master July 30, 2026 21:33
…reorder bug

EffectComposerImpl was created inside useMemo, which doesn't guarantee
its cleanup runs before React discards a memoized value - this leaked
the composer (and its WebGL resources) under StrictMode. It's now
created and disposed inside a useState/useEffect lifecycle instead.

Pass collection walks the real r3f instance tree (group.current.__r3f
.children) and rebuilds the pass list from scratch whenever it or the
composer changes, so the order always matches current JSX - including
through wrapper components - after a reorder or a remount.

While testing the reorder case, found a genuine bug in r3f's host
config: when React moves (not remounts) an existing child - e.g. a
key-preserving reorder of a keyed effect list - insertBefore/
appendChild splice the moved instance into its new slot without
detaching it from the old one first, leaving a stale duplicate in
Instance.children. Worth reporting upstream. Added a cheap dedupe
(keep each object's last occurrence, sorted by that position) that
recovers the correct order either way and is a no-op when the bug
isn't present.

Also widens `children` from JSX.Element | JSX.Element[] to ReactNode,
which was rejecting the common `condition && <Effect/>` idiom (that
expression is `false | Element`, and `false` isn't assignable to
either half of the union). The tree-walk already tolerates arbitrary
children gracefully - it just filters for `instanceof Effect ||
instanceof Pass` - so the stricter type wasn't buying any actual
safety.

Replaces the old root-level EffectComposer.test.tsx (superseded,
predates this suite and used a different mock-root setup) with a
suite covering pass registration/composition order, StrictMode
disposal, and the reorder/dedupe behavior above. (ColorAverage-specific
dispose coverage lands with the primitive-effect dispose fix, a few
commits later - ColorAverage doesn't dispose itself until then.)
@kvvasuu
kvvasuu force-pushed the fix/03-effectcomposer-leak-and-reorder branch from 0d18afa to 76b09e5 Compare July 30, 2026 21:33
@krispya

krispya commented Jul 30, 2026

Copy link
Copy Markdown
Member

The bug was addressed here: pmndrs/react-three-fiber#3808

I'll cut a release after doing some quick hardening checks.

@krispya

krispya commented Jul 31, 2026

Copy link
Copy Markdown
Member

R3F v9.7.0 is out so I think remove the workaround (verify that it works correctly too) and then this is ready to go.

…3f 9.7.0

r3f 9.7.0 fixes the reconciler bug this workaround existed for: internal
instance children were drifting out of sync on a keyed reorder (reported
upstream by us, fixed in pmndrs/react-three-fiber#3808). With the fix in
place, group.current.__r3f.children no longer contains a stale duplicate
after React moves a keyed child, so the dedupe step is no longer needed.

Bumped @react-three/fiber to ^9.7.0 and removed dedupeByLastOccurrence,
reading groupInstance.children directly.

Verified empirically: the existing reorder-with-key test passes on 9.7.0
without the workaround, and fails with the exact same symptom as before
(Expected EffectPass with 2 effects) when temporarily downgraded back to
9.6.1 with the workaround still removed - confirming this is the actual
upstream fix taking effect, not a coincidence.
@kvvasuu

kvvasuu commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Bumped the dependency and dropped dedupeByLastOccurrence accordingly. All tests passed. Also verified empirically.

@kvvasuu
kvvasuu merged commit 3ba4a12 into master Jul 31, 2026
1 check passed
@kvvasuu
kvvasuu deleted the fix/03-effectcomposer-leak-and-reorder branch July 31, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak when switching cameras

2 participants