Skip to main content
Technical preview

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 the Redraw instance: 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, or fallback when 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 only errorFallback set, the server renders nothing in place of the gated children, so the content pops in on hydration; pass a fallback too when the subtree should reserve its layout.
  • <RedrawCanvas> and useSurface render a plain <canvas> server-side (reserving its layout) and start drawing once the instance is ready in the browser.
  • useRedraw is 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, onError fires when initialization fails or when the device is lost (a loss triggers automatic re-initialization).
  • On a canvas, onError fires 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, onReady receives the Redraw instance once it is ready, before the first frame. It is safe under React StrictMode.
  • On a canvas, onGPUTime receives the GPU time of the most recently completed shading pass in nanoseconds after each frame. It requires options={{ timestampQuery: true }} on the provider and a device that supports the timestamp-query feature; when timestamps are unavailable, a one-time warning is logged instead.

Without an onError handler, errors are logged to the console.