When building React components that wrap external resources (e.g., WebGL/canvas), follow these rules:
forwardRef rather than custom ref plumbing.useState/useEffect/useLayoutEffect/useRef must be unconditional.React.ReactElement over JSX.Element in newer React type setups.Example pattern:
export const ShaderMount = forwardRef<HTMLCanvasElement, Props>(function ShaderMountImpl(
{ fragmentShader, uniforms, speed = 1, frame = 0 },
forwardedRef
) {
const canvasRef = typeof forwardedRef === 'function'
? forwardedRef // handle callback refs externally by not assuming .current
: (forwardedRef ?? useRef<HTMLCanvasElement>(null));
const [mount, setMount] = useState<ShaderMountVanilla | null>(null);
useLayoutEffect(() => {
const canvas = (canvasRef as React.RefObject<HTMLCanvasElement> | null)?.current;
if (!canvas || mount) return;
let cancelled = false;
processUniforms(uniforms).then((u) => {
if (cancelled || !canvas) return;
setMount(new ShaderMountVanilla(canvas, fragmentShader, u, /* attrs */ speed, frame));
});
return () => {
cancelled = true;
mount?.dispose();
};
}, [fragmentShader, uniforms, mount, speed, frame]);
return <canvas ref={canvasRef as any} />;
});
Apply this to ensure ref correctness across React versions, prevent hook/ref breakage, and avoid subtle lifecycle/race issues with async WebGL setup and uniform updates.