useRenderQuality
A scene gate decides when a 3D scene should run and how good it should look, but React Three Fiber has no idea any of that happened. It keeps rendering off screen, at whatever pixel ratio the display reports, and it ships no handler for a lost context, so the first time the browser reclaims one the canvas goes black and stays black.
useRenderQuality turns a useSceneGate decision into props you spread onto <Canvas>. Call it outside the canvas, because that is where its output goes.
It does three things. Pixel ratio and shadows follow the scene's state, so a constrained scene draws cheaper without being rebuilt. An idle scene gets frameloop: "never", which stops the loop while keeping the context, its textures and its compiled shaders. And it attaches the context-loss listener React Three Fiber does not, so a dead canvas reports itself and the generation counter can rebuild it.
Since 4.1.0 it also finds the WebGPU device behind the renderer and watches that, so a WebGPU scene recovers the same way. Nothing about how you write the scene changes.
◆Live React Three Fiber Quality Governance
Quick start
Obtain scene state from useSceneGate, generate canvas props with useRenderQuality, and spread them onto <Canvas>.
Never pass conflicting dpr, shadows, or frameloop props to <Canvas> alongside {...canvasProps}.
What the adapter owns, and what your components own
The adapter sets renderer settings. How much there is to draw stays in your own components.
The adapter owns dpr, shadows, frameloop and the loss wiring. Those are properties of the renderer, they live outside the canvas, and it can genuinely apply them.
Instance counts, geometry detail and shader complexity are not renderer settings. They live inside your components, and the gate hands you quality to branch on. Collapsing the two is what produces a scene that is either always heavy or always poor, because the only dial left is resolution.
The props are returned rather than applied because React Three Fiber re-runs its configure pass on every render of <Canvas> and resets those properties from JSX props. A hook that set them imperatively was overwritten a frame later. That was the 4.0.0 change: a renderer asked for dpr: 1 was running at 1.25.
API Reference
Syntax
Converts a SceneGate state into declarative React Three Fiber <Canvas> configuration props.
Parameters
| Parameters.field | Type | Default | Description |
|---|---|---|---|
| state | SceneState | (none) | Active state returned by useSceneGate ('dormant', 'warming', 'active', 'constrained', 'idle', 'recovering', 'poster'). |
| profiles.full | RenderProfile ({ dpr?: number; shadows?: boolean }) | (none) | Renderer configuration applied when state is 'active'. From 4.2.0 its dpr is a ceiling: the canvas never draws above the screen's own ratio. |
| profiles.reduced | RenderProfile ({ dpr?: number; shadows?: boolean }) | (none) | Renderer configuration applied during 'constrained' or 'warming' states. |
Return Value
Props object designed to be spread onto <Canvas {...canvasProps}>.
Properties & State
| Properties.field | Type | Description |
|---|---|---|
| dpr | number | undefined | The active profile's pixel ratio, capped at the screen's own from 4.2.0: dpr: 2 draws a 1x monitor at 1x and a 3x phone at 2. Earlier versions applied it exactly as written. |
| frameloop | "always" | "never" | Set to 'never' when state is 'idle' to halt GPU render cycles. |
| shadows | boolean | undefined | Whether shadow map passes are enabled. |
| onCreated | (state) => void | Callback attaching context loss event listeners to the canvas DOM element. |
Context Loss Recovery & Unmount
The adapter handles canvas recreation and WebGL context restoration cleanly.
Lifecycle & Invariant Guarantees
- →An off-screen scene costs nothing: An idle scene gets frameloop: "never", so the loop stops while the context, its textures and its compiled shaders stay in memory.
- →Resolution holds while a scene is idle: Pixel ratio stops changing once drawing stops, so the last frame survives. Lowering it would resize the drawing buffer, which clears it, and nothing is left running to repaint. Needs 4.0.2 or newer.
- →A dead canvas reports itself: The listener calls preventDefault first, because some drivers refuse a replacement context otherwise, then reports the loss so the generation can move.
- →WebGPU recovers the same way: It also finds the device behind a WebGPU renderer and watches it, and keeps the WebGL listener attached, which is what covers three's silent fallback to WebGL. Needs 4.1.0 or newer.
- →Never above the screen's own resolution: A profile's dpr is a ceiling. dpr: 2 draws a 1x monitor at 1x, where applying it as written shaded four times the pixels the screen can show. Needs 4.2.0 or newer.
- →Closing a canvas is not a failure: React Three Fiber force-loses the context of every canvas it unmounts. The listener ignores a canvas that has left the page and comes off when a rebuild replaces it, so a tab, a modal or a conditional canvas closing no longer rebuilds every other scene. On 4.1 a page with two scenes could rebuild in a loop. Needs 4.2.0 or newer.
- →A loss in the gaps still counts: A context lost while R3F is still building the scene is caught when onCreated runs, and a replacement lost moments after its rebuild triggers another. Before, both left a black canvas. Needs 4.2.0 or newer.
- →Spreading the props does not churn the canvas: The returned object is memoised, so a re-render that changes nothing does not reconfigure the renderer.
◆Production Examples
Battle-tested production patterns ready to copy directly into your codebase.
Integrate SceneGate with RenderQuality for an adaptive full-screen 3D hero.
When NOT to use this
Rules & Gotchas
- →Do not also pass dpr, shadows or frameloop to <Canvas>: React Three Fiber reconciles all three from props on every render, so the later prop wins and the adapter's decision is silently discarded. Quality levels belong in the profiles argument.
- →Put key={scene.generation} on <Canvas> itself: Keying an inner component leaves the dead context mounted, so recovery produces a second dead canvas.
- →Call it outside <Canvas>: It produces props that <Canvas> consumes. Called inside the tree it is asking for context that does not exist there yet.
- →Spread the whole object: Picking fields out of it means picking which of its four jobs still happen, and the one people drop is onCreated, which is the loss handling.