Skip to content

fix(🌐): keep the web canvas usable when its layout effect re-runs - #4027

Open
dennytosp wants to merge 1 commit into
Shopify:mainfrom
dennytosp:fix/keep-web-canvas-context-on-effect-rerun
Open

fix(🌐): keep the web canvas usable when its layout effect re-runs#4027
dennytosp wants to merge 1 commit into
Shopify:mainfrom
dennytosp:fix/keep-web-canvas-context-on-effect-rerun

Conversation

@dennytosp

Copy link
Copy Markdown
Contributor

Fixes #3976.

WebGLRenderer.dispose() calls WEBGL_lose_context.loseContext() on the <canvas> element. Per the spec that loses the element's context permanently — a later getContext("webgl2") returns the same dead context object, and only restoreContext() revives it.

But the renderer is created and disposed inside a useLayoutEffect, and effect cleanup does not mean the element is going away. React re-runs layout effects on a preserved host node in at least two everyday cases:

  • StrictMode's DEV double-invoke, on every mount in development;
  • reappearLayoutEffects when a hidden Activity/offscreen subtree is revealed — which is how React Navigation and Expo Router keep inactive routes mounted.

The second construction then hands CanvasKit a dead canvas and faults inside wasm with Cannot read properties of null (reading 'rangeMin'), taking the tree down.

What #3929 actually needed is CanvasKit.deleteContext, which unregisters the context from CanvasKit's registry and drops its reference to the detached element — that stays. loseContext() only released the drawing buffer eagerly, and it was the sole part of dispose() that left the element unusable. StaticWebGLRenderer.cleanupRenderResult() keeps its call: that one is on a throwaway OffscreenCanvas that is never reused.

The new test in SkiaPictureView.web.spec.tsx drives the component through StrictMode with a WebGL context mock that models the spec'd behaviour — once lost, the element's context stays lost, and GetWebGLContext returns no handle for it. On main it fails with the reported stack:

Could not create a WebGL context
  at new WebGLRenderer (src/views/SkiaPictureView.web.tsx:48:13)
  at commitHookLayoutEffects
  at reappearLayoutEffects
  at doubleInvokeEffectsOnFiber
  at recursivelyTraverseAndDoubleInvokeEffectsInDEV

The mock is wired into the existing getContext stub, so the four pre-existing tests now exercise a real context object instead of null; they still pass unchanged.

WebGLRenderer.dispose() called WEBGL_lose_context.loseContext() on the <canvas>
element, which per spec loses that element's context permanently - a later
getContext() returns the same dead context and only restoreContext() revives
it. But the renderer lives in a layout effect, and effect cleanup does not mean
the element is going away: StrictMode's DEV double-invoke and an Activity
reveal (how React Navigation keeps inactive routes mounted) both re-run the
effect on the very same node. The second construction then hands CanvasKit a
dead canvas and faults inside wasm with "Cannot read properties of null
(reading 'rangeMin')".

What Shopify#3929 needed was CanvasKit.deleteContext, which unregisters the context
and drops the reference to the detached element - that stays. loseContext only
released the drawing buffer eagerly, and it was the one part of dispose() that
left the element unusable.

The regression test drives the component through StrictMode with a WebGL
context mock that models the spec'd behaviour: once lost, the element's context
stays lost.

Fixes Shopify#3976
@dennytosp
dennytosp force-pushed the fix/keep-web-canvas-context-on-effect-rerun branch from 05cc2b4 to 0101224 Compare August 24, 2026 15:11
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.

[Web] Canvas crashes on layout-effect re-run: dispose() loses the reused canvas element's WebGL context (MakeWebGLContext null, 'rangeMin')

1 participant