[]previous and next page

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#

Board.tsxtsx
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.

Studio.tsxtsx
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):

tsx
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:

StickyChart.tsxtsx
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.

app/board/[id]/page.tsxtsx
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:

Board.tsxtsx
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' }}
    />
  )
}