From 9f798fcd56caba9716c8d8f031769b6ae8acbf33 Mon Sep 17 00:00:00 2001 From: kvvasuu Date: Fri, 31 Jul 2026 22:23:39 +0200 Subject: [PATCH] fix(Autofocus): guard depthPickingPass/copyPass against double dispose EffectComposerImpl.dispose() disposes every pass it holds, including these two added via composer.addPass - if Autofocus unmounts alongside its ancestor EffectComposer, its own cleanup disposed them a second time. This collision only becomes reachable now that the composer lifecycle fix (previous commit) actually disposes the composer on unmount - previously it leaked and never disposed at all, so it never collided with Autofocus's own cleanup. --- src/effects/Autofocus.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/effects/Autofocus.tsx b/src/effects/Autofocus.tsx index 522c1a53..fefcad56 100644 --- a/src/effects/Autofocus.tsx +++ b/src/effects/Autofocus.tsx @@ -18,6 +18,23 @@ import { Mesh, Vector3 } from 'three' import { EffectComposerContext } from '../EffectComposer' import { DepthOfField } from './DepthOfField' +// EffectComposerImpl.dispose() disposes every pass it currently holds — +// including these two, since they're added via composer.addPass below. +// When Autofocus unmounts alongside its ancestor EffectComposer (e.g. a +// full tree unmount), both the composer's own teardown AND this +// component's cleanup effect would dispose the same instances. Wrapping +// dispose here makes it safe no matter which caller gets there first. +function makeDisposeIdempotent void }>(instance: T): T { + let disposed = false + const dispose = instance.dispose.bind(instance) + instance.dispose = () => { + if (disposed) return + disposed = true + dispose() + } + return instance +} + export type AutofocusProps = ComponentProps & { target?: R3FVector3 /** should the target follow the pointer */ @@ -55,8 +72,8 @@ export function Autofocus({ const { composer, camera } = useContext(EffectComposerContext) // see: https://codesandbox.io/s/depthpickingpass-x130hg - const [depthPickingPass] = useState(() => new DepthPickingPass()) - const [copyPass] = useState(() => new CopyPass()) + const [depthPickingPass] = useState(() => makeDisposeIdempotent(new DepthPickingPass())) + const [copyPass] = useState(() => makeDisposeIdempotent(new CopyPass())) useEffect(() => { composer.addPass(depthPickingPass) composer.addPass(copyPass)