React and Next.js
The CanvasFlow component, the headless variant, hooks for reading editor state, and React content inside shapes.
canvasflow/react is a thin layer over the DOM host. The editor still does the work; React gets a component to mount it and hooks to read from it.
The component#
import { CanvasFlow } from 'canvasflow/react'
export function Board() {
return (
<CanvasFlow
persistenceKey="board"
onMount={(editor, app, ui) => {
editor.zoomToFit()
return () => console.log('unmounted') // optional cleanup
}}
style={{ height: '100vh' }}
/>
)
}
<CanvasFlow/> takes every option createCanvasFlow takes (see Options), plus:
| Prop | What it does |
|---|---|
ui |
true (default), false, or the default UI's options object |
onMount |
Called once with (editor, app, ui). Return a function to clean up |
className, style |
For the wrapper element |
children |
Rendered inside the canvas, where the hooks work |
Headless: <CanvasFlowCanvas/>#
When you build your own interface, use <CanvasFlowCanvas/>. It's the same component without the default UI, and it doesn't reference canvasflow/ui at all, so none of it reaches your bundle.
import { CanvasFlowCanvas, useEditor, useValue } from 'canvasflow/react'
export function Studio() {
return (
<CanvasFlowCanvas style={{ height: '100vh' }}>
<Toolbar />
</CanvasFlowCanvas>
)
}
function Toolbar() {
const editor = useEditor()
const tool = useValue((e) => e.state.toolId)
return (
<div className="toolbar">
{['select', 'draw', 'rectangle', 'arrow'].map((id) => (
<button key={id} aria-pressed={tool === id} onClick={() => editor.setTool(id)}>
{id}
</button>
))}
</div>
)
}
Hooks#
All hooks work inside <CanvasFlow>, <CanvasFlowCanvas> or <CanvasFlowProvider>.
| Hook | Returns |
|---|---|
useEditor() |
The editor |
useValue(selector, isEqual?) |
A value derived from the editor, re-rendering only when it changes |
useEditorEvent(name, handler) |
Nothing. Subscribes to an event for the component's lifetime |
useCanvasFlow() |
{ editor, app, ui } |
useValue is the one you'll use most. The selector runs after every change, and your component re-renders only when its result changes (by Object.is, or your own isEqual):
const count = useValue((e) => e.getCurrentPageShapes().length)
const canUndo = useValue((e) => e.canUndo)
const zoom = useValue((e) => Math.round(e.camera.z * 100))
React inside shapes#
A shape can render React. Return reactNode from its render, and the DOM host mounts your component inside the shape, sized and transformed with it:
import { defineShape, Geometry, rectVertices } from 'canvasflow'
import { reactNode } from 'canvasflow/react'
import { Chart } from './Chart'
export const ChartShape = defineShape({
type: 'chart',
props: () => ({ w: 320, h: 200, series: [3, 7, 4, 9] }),
geometry: (shape) => new Geometry(rectVertices(shape.props.w, shape.props.h), { isClosed: true, isFilled: true }),
render: (shape) =>
reactNode({ key: shape.id, w: shape.props.w, h: shape.props.h, interactive: true, element: <Chart data={shape.props.series} /> }),
})
Register it like any shape: <CanvasFlow shapes={[ChartShape]} />.
Next.js#
The React entry ships with 'use client', so you can import it from a server component's tree without wrapping it yourself. On the server, the component renders an empty, correctly sized box; the canvas mounts in the browser. No dynamic(…, { ssr: false }) needed.
import { Board } from './Board' // a 'use client' file that renders <CanvasFlow/>
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
return <Board key={id} id={id} />
}
Keying the board by id remounts it cleanly when you navigate between boards.
Wiring things up on mount#
Anything that needs the editor (sync, analytics, your own shortcuts) goes in onMount, and its cleanup in the function you return. It runs once per mount, before the first interaction:
import { CanvasFlow } from 'canvasflow/react'
import { connect } from 'canvasflow/collab'
export function Board({ id }: { id: string }) {
return (
<CanvasFlow
key={id}
onMount={(editor) => {
const client = connect(editor, { room: id })
return () => client.stop()
}}
style={{ height: '100vh' }}
/>
)
}