events.connect throws TypeError on null target during DOM-reparenting animations

Author: haxiomicCreated May 6, 2026Updated Sep 8, 2026
Labelsbugevents

Describe the bug

createPointerEvents.connect (in events.ts) does target.addEventListener(...) with no null-guard, while the matching disconnect IS null-guarded via if (events.connected). Under certain re-render conditions, r3f calls events.connect(target) with target == null and crashes with:

TypeError: Cannot read properties of null (reading 'addEventListener')
  at Object.connect (events.esm.js:16157)
  at CanvasImpl.useIsomorphicLayoutEffect.run
  at Provider.useIsomorphicLayoutEffect

I see this 23+ times per fullscreen entry/exit when a <Canvas> subtree is DOM-reparented during a spring animation (using a Fullscreenable pattern that calls appendChild between two containers each frame). Plus 2 errors at initial mount in some component arrangements.

Repro

Minimal-ish: any setup where the parent of <Canvas> is being re-rendered rapidly during a transition that toggles state inside the parent (forcing CanvasImpl's no-deps useIsomorphicLayoutEffect to re-run + its Bridge to potentially remount). Concretely: a custom fullscreen wrapper that reparents the <Canvas> subtree via appendChild while also driving setState on each animation frame.

Verified in r3f 9.5.0.

Source

  • react-three-fiber.esm.js, CanvasImpl at line 12: useIsomorphicLayoutEffect has no deps, fires on every render. await root.current.configure(...) then root.current.render(...). The onCreated callback (line 86) calls state.events.connect(eventSource ? ... : divRef.current).
  • events-5a94e5eb.esm.js, createPointerEvents.connect (line 16141): unconditionally calls target.addEventListener(...).
  • Provider.useIsomorphicLayoutEffect (line 15795): also calls state.events.connect(rootElement). Guarded against repeat connects via events.connected check, so this typically fires at most once per Provider mount.

Root cause

The per-frame trigger is Provider remounting during the animation, most plausibly because useBridge (line 51-65) memoizes on [fiber, ContextBridge] and ContextBridge from its-fine's useContextBridge() can return a new component reference under context churn. When Bridge's component identity changes, reconciler.updateContainer unmounts/remounts Provider → its [] mount-effect re-fires → invokes user-onCreated → CanvasImpl's wrapped onCreated calls events.connect(divRef.current). During the animation divRef.current can be transiently null between renders.

Proposed fix

Mirror the existing disconnect null-guard onto connect:

typescript
connect: (target: HTMLElement | null) => {
  if (!target) return
  // ...rest unchanged
}

And update the TS signature to allow null:

typescript
connect?: (target: TTarget | null) => void

Skipping a null-target call is safe: the next non-null connect(target) calls disconnect() first (no-op when connected is undefined) and re-attaches all listeners cleanly. No leak, no missed listeners.

Why I think this matters even though there's a userland workaround

Userland can wrap r3f's default events factory with a null-guard (which is what I'm doing), but that's

  1. coupling consumer code to an undocumented r3f internal contract,
  2. tied to r3f's EventManager shape, so a future r3f minor that touches that shape would silently drop the guard,
  3. forcing every host wrapping <Canvas> (SharedCanvas-style multi-pane integrations are increasingly common) to reproduce the same shim.

Closing #3544 noted that the analogous symptom (containerRect 0×0 during animation) was resolved with a small defensiveness change. This is the same architectural cause showing up in a slightly different code path.

Reproduction

I can put together a minimal CodeSandbox repro if helpful — please let me know if that would speed review.

Source: pmndrs/react-three-fiber