Skip to content

Commit 6db4025

Browse files
committed
fix: dispose <primitive>-based effects, r3f never does it for them
r3f explicitly never auto-disposes objects rendered via <primitive object={...}> ("their state may be kept outside of React"), regardless of dispose={null}. Several effects that render this way had no cleanup at all (ASCII, ColorAverage, SelectiveBloom, SSAO), so they leaked their underlying postprocessing effect/texture on every unmount and every prop change that recreates the instance. Added a small useDispose hook (util.tsx) and wired it into every <primitive>-based effect, with a WeakSet guard against double-dispose across StrictMode's dev-only mount/cleanup/mount cycle. Also fixes GodRays, which was declared as (props, ref) without forwardRef - under React 19 that ref parameter is never populated, so consumers passing a ref to GodRays silently got nothing. ChromaticAberration moves off wrapEffect onto the same manual construct-and-dispose pattern so its tuple `offset` prop coerces through useVector2 like the other vector-typed effects; test locks in that coercion. Adds the ColorAverage dispose coverage to EffectComposer.test.tsx that the previous commit's suite deferred here, since ColorAverage didn't dispose itself until this fix landed.
1 parent e61f405 commit 6db4025

17 files changed

Lines changed: 415 additions & 246 deletions

src/effects/ASCII.tsx

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
// From: https://github.com/emilwidlund/ASCII
22
// https://twitter.com/emilwidlund/status/1652386482420609024
33

4-
import { forwardRef, useMemo } from 'react'
5-
import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
64
import { Effect } from 'postprocessing'
5+
import { Ref, useMemo } from 'react'
6+
import { CanvasTexture, Color, NearestFilter, RepeatWrapping, Texture, Uniform } from 'three'
7+
import { useDispose } from '../util'
78

8-
const fragment = `
9-
uniform sampler2D uCharacters;
10-
uniform float uCharactersCount;
11-
uniform float uCellSize;
12-
uniform bool uInvert;
13-
uniform vec3 uColor;
9+
const fragment = /* glsl */ `
10+
uniform sampler2D uCharacters;
11+
uniform float uCharactersCount;
12+
uniform float uCellSize;
13+
uniform bool uInvert;
14+
uniform vec3 uColor;
1415
15-
const vec2 SIZE = vec2(16.);
16+
const vec2 SIZE = vec2(16.);
1617
17-
vec3 greyscale(vec3 color, float strength) {
18+
vec3 greyscale(vec3 color, float strength) {
1819
float g = dot(color, vec3(0.299, 0.587, 0.114));
1920
return mix(color, vec3(g), strength);
20-
}
21+
}
2122
22-
vec3 greyscale(vec3 color) {
23+
vec3 greyscale(vec3 color) {
2324
return greyscale(color, 1.0);
24-
}
25+
}
2526
26-
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
27+
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
2728
vec2 cell = resolution / uCellSize;
2829
vec2 grid = 1.0 / cell;
2930
vec2 pixelizedUV = grid * (0.5 + floor(uv / grid));
@@ -43,7 +44,7 @@ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor)
4344
asciiCharacter.rgb = uColor * asciiCharacter.r;
4445
asciiCharacter.a = pixelized.a;
4546
outputColor = asciiCharacter;
46-
}
47+
}
4748
`
4849

4950
interface IASCIIEffectProps {
@@ -53,6 +54,7 @@ interface IASCIIEffectProps {
5354
cellSize?: number
5455
color?: string
5556
invert?: boolean
57+
ref?: Ref<ASCIIEffect>
5658
}
5759

5860
class ASCIIEffect extends Effect {
@@ -63,7 +65,7 @@ class ASCIIEffect extends Effect {
6365
cellSize = 16,
6466
color = '#ffffff',
6567
invert = false,
66-
}: IASCIIEffectProps = {}) {
68+
}: Omit<IASCIIEffectProps, 'ref'> = {}) {
6769
const uniforms = new Map<string, Uniform>([
6870
['uCharacters', new Uniform(new Texture())],
6971
['uCellSize', new Uniform(cellSize)],
@@ -114,22 +116,21 @@ class ASCIIEffect extends Effect {
114116
}
115117
}
116118

117-
export const ASCII = /* @__PURE__ */ forwardRef<ASCIIEffect, IASCIIEffectProps>(
118-
(
119-
{
120-
font = 'arial',
121-
characters = ` .:,'-^=*+?!|0#X%WM@`,
122-
fontSize = 54,
123-
cellSize = 16,
124-
color = '#ffffff',
125-
invert = false,
126-
},
127-
fref
128-
) => {
129-
const effect = useMemo(
130-
() => new ASCIIEffect({ characters, font, fontSize, cellSize, color, invert }),
131-
[characters, fontSize, cellSize, color, invert, font]
132-
)
133-
return <primitive ref={fref} object={effect} />
134-
}
135-
)
119+
export function ASCII({
120+
font = 'arial',
121+
characters = ` .:,'-^=*+?!|0#X%WM@`,
122+
fontSize = 54,
123+
cellSize = 16,
124+
color = '#ffffff',
125+
invert = false,
126+
ref,
127+
}: IASCIIEffectProps) {
128+
const effect = useMemo(
129+
() => new ASCIIEffect({ characters, font, fontSize, cellSize, color, invert }),
130+
[characters, fontSize, cellSize, color, invert, font]
131+
)
132+
133+
useDispose(effect)
134+
135+
return <primitive ref={ref} object={effect} />
136+
}
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
1+
import type { ReactThreeFiber } from '@react-three/fiber'
12
import { ChromaticAberrationEffect } from 'postprocessing'
2-
import { type EffectProps, wrapEffect } from '../wrapEffect'
3+
import type { Ref } from 'react'
4+
import { useMemo } from 'react'
5+
import { useDispose, useVector2 } from '../util'
36

4-
export type ChromaticAberrationProps = EffectProps<typeof ChromaticAberrationEffect>
5-
export const ChromaticAberration = /* @__PURE__ */ wrapEffect(ChromaticAberrationEffect)
7+
export type ChromaticAberrationProps = Omit<
8+
Partial<ConstructorParameters<typeof ChromaticAberrationEffect>[0]>,
9+
'offset'
10+
> & {
11+
ref?: Ref<ChromaticAberrationEffect>
12+
offset?: ReactThreeFiber.Vector2
13+
}
14+
15+
export function ChromaticAberration({ ref, ...props }: ChromaticAberrationProps) {
16+
const offset = useVector2(props, 'offset')
17+
18+
const effect = useMemo(
19+
() =>
20+
new ChromaticAberrationEffect({
21+
...props,
22+
offset,
23+
} as ConstructorParameters<typeof ChromaticAberrationEffect>[0]),
24+
[offset, props]
25+
)
26+
27+
useDispose(effect)
28+
29+
return <primitive object={effect} ref={ref} />
30+
}

src/effects/ColorAverage.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import { ColorAverageEffect, BlendFunction } from 'postprocessing'
2-
import React, { Ref, forwardRef, useMemo } from 'react'
3-
4-
export type ColorAverageProps = Partial<{
5-
blendFunction: BlendFunction
6-
}>
7-
8-
export const ColorAverage = /* @__PURE__ */ forwardRef<ColorAverageEffect, ColorAverageProps>(function ColorAverage(
9-
{ blendFunction = BlendFunction.NORMAL }: ColorAverageProps,
10-
ref: Ref<ColorAverageEffect>
11-
) {
12-
/** Because ColorAverage blendFunction is not an object but a number, we have to define a custom prop "blendFunction" */
1+
import { BlendFunction, ColorAverageEffect } from 'postprocessing'
2+
import type { Ref } from 'react'
3+
import { useMemo } from 'react'
4+
import { useDispose } from '../util'
5+
6+
export type ColorAverageProps = {
7+
blendFunction?: BlendFunction
8+
ref?: Ref<ColorAverageEffect>
9+
}
10+
11+
export function ColorAverage({ blendFunction = BlendFunction.NORMAL, ref }: ColorAverageProps) {
1312
const effect = useMemo(() => new ColorAverageEffect(blendFunction), [blendFunction])
14-
return <primitive ref={ref} object={effect} dispose={null} />
15-
})
13+
14+
useDispose(effect)
15+
16+
return <primitive object={effect} ref={ref} />
17+
}

src/effects/DepthOfField.tsx

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import type { ReactThreeFiber } from '@react-three/fiber'
12
import { DepthOfFieldEffect, MaskFunction } from 'postprocessing'
2-
import { Ref, forwardRef, useMemo, useEffect, useContext } from 'react'
3-
import { ReactThreeFiber } from '@react-three/fiber'
3+
import type { Ref } from 'react'
4+
import { use, useMemo } from 'react'
45
import { type DepthPackingStrategies, type Texture, Vector3 } from 'three'
56
import { EffectComposerContext } from '../EffectComposer'
7+
import { useDispose } from '../util'
68

7-
type DOFProps = ConstructorParameters<typeof DepthOfFieldEffect>[1] &
9+
export type DepthOfFieldProps = ConstructorParameters<typeof DepthOfFieldEffect>[1] &
810
Partial<{
11+
ref: Ref<DepthOfFieldEffect>
912
target: ReactThreeFiber.Vector3
1013
depthTexture: {
1114
texture: Texture
@@ -16,28 +19,27 @@ type DOFProps = ConstructorParameters<typeof DepthOfFieldEffect>[1] &
1619
blur: number
1720
}>
1821

19-
export const DepthOfField = /* @__PURE__ */ forwardRef(function DepthOfField(
20-
{
21-
blendFunction,
22-
worldFocusDistance,
23-
worldFocusRange,
24-
focusDistance,
25-
focusRange,
26-
focalLength,
27-
bokehScale,
28-
resolutionScale,
29-
resolutionX,
30-
resolutionY,
31-
width,
32-
height,
33-
target,
34-
depthTexture,
35-
...props
36-
}: DOFProps,
37-
ref: Ref<DepthOfFieldEffect>
38-
) {
39-
const { camera } = useContext(EffectComposerContext)
22+
export function DepthOfField({
23+
ref,
24+
blendFunction,
25+
worldFocusDistance,
26+
worldFocusRange,
27+
focusDistance,
28+
focusRange,
29+
focalLength,
30+
bokehScale,
31+
resolutionScale,
32+
resolutionX,
33+
resolutionY,
34+
width,
35+
height,
36+
target,
37+
depthTexture,
38+
...props
39+
}: DepthOfFieldProps) {
40+
const { camera } = use(EffectComposerContext)
4041
const autoFocus = target != null
42+
4143
const effect = useMemo(() => {
4244
const effect = new DepthOfFieldEffect(camera, {
4345
blendFunction,
@@ -79,11 +81,7 @@ export const DepthOfField = /* @__PURE__ */ forwardRef(function DepthOfField(
7981
depthTexture,
8082
])
8183

82-
useEffect(() => {
83-
return () => {
84-
effect.dispose()
85-
}
86-
}, [effect])
84+
useDispose(effect)
8785

88-
return <primitive {...props} ref={ref} object={effect} target={target} />
89-
})
86+
return <primitive {...props} object={effect} ref={ref} target={target} />
87+
}

src/effects/Glitch.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Vector2 } from 'three'
2-
import { GlitchEffect, GlitchMode } from 'postprocessing'
3-
import { Ref, forwardRef, useMemo, useLayoutEffect, useEffect } from 'react'
41
import { ReactThreeFiber, useThree } from '@react-three/fiber'
5-
import { useVector2 } from '../util'
2+
import { GlitchEffect, GlitchMode } from 'postprocessing'
3+
import { Ref, useLayoutEffect, useMemo } from 'react'
4+
import { useDispose, useVector2 } from '../util'
65

76
export type GlitchProps = ConstructorParameters<typeof GlitchEffect>[0] &
87
Partial<{
@@ -12,29 +11,27 @@ export type GlitchProps = ConstructorParameters<typeof GlitchEffect>[0] &
1211
duration: ReactThreeFiber.Vector2
1312
chromaticAberrationOffset: ReactThreeFiber.Vector2
1413
strength: ReactThreeFiber.Vector2
14+
ref?: Ref<GlitchEffect>
1515
}>
1616

17-
export const Glitch = /* @__PURE__ */ forwardRef<GlitchEffect, GlitchProps>(function Glitch(
18-
{ active = true, ...props }: GlitchProps,
19-
ref: Ref<GlitchEffect>
20-
) {
17+
export function Glitch({ active = true, ref, ...props }: GlitchProps) {
2118
const invalidate = useThree((state) => state.invalidate)
2219
const delay = useVector2(props, 'delay')
2320
const duration = useVector2(props, 'duration')
2421
const strength = useVector2(props, 'strength')
2522
const chromaticAberrationOffset = useVector2(props, 'chromaticAberrationOffset')
23+
2624
const effect = useMemo(
2725
() => new GlitchEffect({ ...props, delay, duration, strength, chromaticAberrationOffset }),
2826
[delay, duration, props, strength, chromaticAberrationOffset]
2927
)
28+
3029
useLayoutEffect(() => {
3130
effect.mode = active ? props.mode || GlitchMode.SPORADIC : GlitchMode.DISABLED
3231
invalidate()
3332
}, [active, effect, invalidate, props.mode])
34-
useEffect(() => {
35-
return () => {
36-
effect.dispose?.()
37-
}
38-
}, [effect])
39-
return <primitive ref={ref} object={effect} dispose={null} />
40-
})
33+
34+
useDispose(effect)
35+
36+
return <primitive ref={ref} object={effect} />
37+
}

src/effects/GodRays.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { GodRaysEffect } from 'postprocessing'
2-
import React, { Ref, forwardRef, useMemo, useContext, useLayoutEffect } from 'react'
2+
import { Ref, RefObject, useContext, useLayoutEffect, useMemo } from 'react'
33
import { Mesh, Points } from 'three'
44
import { EffectComposerContext } from '../EffectComposer'
5-
import { resolveRef } from '../util'
5+
import { resolveRef, useDispose } from '../util'
66

77
type GodRaysProps = ConstructorParameters<typeof GodRaysEffect>[2] & {
8-
sun: Mesh | Points | React.RefObject<Mesh | Points>
8+
sun: Mesh | Points | RefObject<Mesh | Points>
9+
ref?: Ref<GodRaysEffect>
910
}
1011

11-
export const GodRays = /* @__PURE__ */ forwardRef(function GodRays(props: GodRaysProps, ref: Ref<GodRaysEffect>) {
12+
export function GodRays({ ref, ...props }: GodRaysProps) {
1213
const { camera } = useContext(EffectComposerContext)
1314
const effect = useMemo(() => new GodRaysEffect(camera, resolveRef(props.sun), props), [camera, props])
1415
useLayoutEffect(() => void (effect.lightSource = resolveRef(props.sun)), [effect, props.sun])
15-
return <primitive ref={ref} object={effect} dispose={null} />
16-
})
16+
17+
useDispose(effect)
18+
19+
return <primitive ref={ref} object={effect} />
20+
}

src/effects/Grid.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
1-
import React, { Ref, forwardRef, useMemo, useLayoutEffect } from 'react'
2-
import { GridEffect } from 'postprocessing'
31
import { useThree } from '@react-three/fiber'
2+
import { GridEffect } from 'postprocessing'
3+
import { Ref, useLayoutEffect, useMemo } from 'react'
4+
import { useDispose } from '../util'
45

56
type GridProps = ConstructorParameters<typeof GridEffect>[0] &
67
Partial<{
78
size: {
89
width: number
910
height: number
1011
}
12+
ref: Ref<GridEffect>
1113
}>
1214

13-
export const Grid = /* @__PURE__ */ forwardRef(function Grid({ size, ...props }: GridProps, ref: Ref<GridEffect>) {
15+
export function Grid({ size, ref, ...props }: GridProps) {
1416
const invalidate = useThree((state) => state.invalidate)
17+
1518
const effect = useMemo(() => new GridEffect(props), [props])
19+
1620
useLayoutEffect(() => {
1721
if (size) effect.setSize(size.width, size.height)
1822
invalidate()
1923
}, [effect, size, invalidate])
20-
return <primitive ref={ref} object={effect} dispose={null} />
21-
})
24+
25+
useDispose(effect)
26+
27+
return <primitive ref={ref} object={effect} />
28+
}

0 commit comments

Comments
 (0)