Redraw is currently in technical preview, available to wcandillon.dev subscribers. API is unstable.
React
react-redraw provides React bindings for Redraw, built around two
pieces:
<RedrawProvider>owns theRedrawinstance: the GPU device and the rendering backend, shared by everything below it.<RedrawCanvas>renders a scene through that instance.
Every canvas and hook in this package needs a <RedrawProvider> above
it (or an explicit instance via the redraw prop) and throws a clear
error otherwise. Wrap your app, or the part of it that draws, once:
import { RedrawCanvas, RedrawProvider } from "react-redraw";
import { render, animate } from "./hello-world";
export function App() {
return (
<RedrawProvider>
<Hello />
</RedrawProvider>
);
}
function Hello() {
return (
<RedrawCanvas
style={{ width: "100%", aspectRatio: "4 / 3" }}
render={render}
animate={animate}
/>
);
}
No matter how many canvases live under the provider, the page holds a single GPU device and shares one set of pipeline caches.
The provider accepts every RedrawInit() option through its options
prop, for example GPU timestamp queries or an existing device:
<RedrawProvider options={{ timestampQuery: true }}>...</RedrawProvider>
// or interop with an existing WebGPU setup; the device stays yours
<RedrawProvider options={{ device }}>...</RedrawProvider>
Nesting
Nesting <RedrawProvider> is idempotent.
If you do want an isolated instance, e.g. a separate device or distinct
options such as timestampQuery, use <LocalRedrawProvider>.
<RedrawProvider>
<RedrawCanvas render={render} animate={animate} />
<LocalRedrawProvider options={{ timestampQuery: true }}>
{/* its own device, isolated from the provider above */}
<RedrawCanvas render={render} animate={animate} />
</LocalRedrawProvider>
</RedrawProvider>
RedrawCanvas
Pass the same render and animate functions you would use with the
core library (see Hello World): render runs once
to build the scene (and again when the canvas resizes); animate runs
every frame and mutates the nodes returned by render.
For static scenes or immediate-mode rendering, use onDraw instead. It
receives a fresh Canvas on every call; pass loop to run it every
frame:
<RedrawCanvas
onDraw={(canvas, width, height, ctx) => {
// rebuild the scene every frame, ctx.time drives the animation
}}
loop
/>
Loading and error states
The provider initializes asynchronously. By default children render
immediately: each canvas mounts (reserving its layout) and starts drawing
once the instance is ready. To show loading and unsupported states
instead, pass fallback and errorFallback:
<RedrawProvider
fallback={<Spinner />}
errorFallback={(error) => <p>WebGPU is not available: {error.message}</p>}
>
<RedrawCanvas render={render} animate={animate} />
</RedrawProvider>
If the device is ever lost, the provider notifies onError, shows
fallback again, and re-initializes with a fresh device; canvases resume
automatically.
useRedraw
Canvases are not the only consumers of the instance: useRedraw() hands
you the Redraw instance directly so you can work with the same device
and backend imperatively, for example to render offscreen, load
textures, or reach the device for interop:
function Thumbnail() {
const redraw = useRedraw();
// Same device and pipeline caches as the canvases on the page
const surface = redraw.makeSurfaceOffscreen(256, 256);
// ... draw to a Canvas, surface.flush(canvas), read pixels ...
}
The hook suspends while the provider initializes (and rethrows init
failures), so components using it work with your own <Suspense> and
error boundaries anywhere below the provider. It is client-only: see
Server-side rendering for how to gate it in an
SSR app.
useSurface
When you want to drive rendering yourself but stay in React,
useSurface manages the canvas element and a surface, rendering
through the provider's instance:
import { useSurface } from "react-redraw";
function MyComponent() {
const { ref, surface } = useSurface();
useEffect(() => {
if (!surface) return;
const canvas = new Canvas();
// ... draw to canvas
surface.flush(canvas);
}, [surface]);
return <canvas ref={ref} />;
}
Like <RedrawCanvas>, it requires a <RedrawProvider> (or a redraw
option) and throws otherwise.
Bring your own instance
You can also manage an instance yourself with RedrawInit() and pass it
explicitly, which overrides any surrounding provider:
import { RedrawInit } from "redraw";
import { RedrawCanvas } from "react-redraw";
const redraw = await RedrawInit();
<RedrawCanvas redraw={redraw} render={render} animate={animate} />;
Ownership is simple: whoever creates the instance owns its device. A canvas only ever destroys its surface; the provider destroys the device it created on unmount; an instance you pass in stays yours.
Server-side rendering
The bindings are safe to render on the server (Next.js, React Router streaming SSR, and similar):
<RedrawProvider>initializes WebGPU only after mounting in the browser. On the server it renders its children directly, orfallbackwhen you passed one, without suspending. Hydration is seamless: the client first renders the same fallback, then swaps in the children once the instance is ready. With onlyerrorFallbackset, the server renders nothing in place of the gated children, so the content pops in on hydration; pass afallbacktoo when the subtree should reserve its layout.<RedrawCanvas>anduseSurfacerender a plain<canvas>server-side (reserving its layout) and start drawing once the instance is ready in the browser.useRedrawis client-only. The instance can never exist on the server, so instead of suspending forever (which would hold a streaming response open until the framework aborts it), the hook throws a descriptive error during server rendering.
To use useRedraw in a component that takes part in SSR, render it only
after a client mount, the standard gating pattern:
function Thumbnail() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
return <Placeholder />; // emitted into the SSR payload
}
return (
<Suspense fallback={<Placeholder />}>
<ThumbnailContent /> {/* calls useRedraw() */}
</Suspense>
);
}
Alternatively, the provider's fallback prop gives you the same gating
for an entire subtree without any wiring.
Real content in the SSR payload
Placeholders do not have to be empty. If your server runtime supports WebGPU (Deno does natively; Node can through the Dawn bindings), the core library runs there too, and you can render the actual scene to an image ahead of time. This happens outside React rendering, in a loader or route handler, since the hooks are client-only:
import { RedrawInit, Canvas } from "redraw";
const redraw = await RedrawInit();
const surface = redraw.makeSurfaceOffscreen(width, height);
const canvas = new Canvas();
render(canvas, width, height);
surface.flush(canvas);
const image = await surface.getImageData(); // encode to PNG, cache, serve
Serve the encoded image and use it as the placeholder (the provider's
fallback, or a <img>/CSS background behind the canvas), and the
server response shows the real content immediately; the live canvas
takes over once WebGPU initializes in the browser.
Lifecycle callbacks
- On the provider,
onErrorfires when initialization fails or when the device is lost (a loss triggers automatic re-initialization). - On a canvas,
onErrorfires when the device is lost or when rendering a frame throws; the animation loop stops first, so a dying device never leaves a frozen canvas without a signal. - On a canvas,
onReadyreceives theRedrawinstance once it is ready, before the first frame. It is safe under React StrictMode. - On a canvas,
onGPUTimereceives the GPU time of the most recently completed shading pass in nanoseconds after each frame. It requiresoptions={{ timestampQuery: true }}on the provider and a device that supports thetimestamp-queryfeature; when timestamps are unavailable, a one-time warning is logged instead.
Without an onError handler, errors are logged to the console.