---
title: "Chapter 8: The React surface"
description: "Run the meeting-notes editor on React: EditorContextProvider, EditorSurface, EditorToolbar, SimpleEditor and the Toaster, and where the callout would go."
canonical_url: "https://skaile-ai.github.io/forge-editor/tutorial/react-surface"
---
# Chapter 8: The React surface

> Run the meeting-notes editor on React: EditorContextProvider, EditorSurface, EditorToolbar, SimpleEditor and the Toaster, and where the callout would go.

Everything in chapters 2 to 7 has a React twin. The React layer (`@skaile/forge-editor/react`, shadcn/ui on Radix and Tailwind 4) renders the same framework-free core as the Vue layer: the simple editor, `EditorToolbar`, the settings panel and modal, and the `@`, `#` and `/` suggestion popups. It uses no Vue, Nuxt UI or Nuxt module.

The runnable reference is `examples/react-simple` (port 3041); it is built into this page. It has the same sample picker, read-only switch, host `/datum` command and JSON and Markdown panes as the Vue example from chapter 2, plus `@` mentions:

<demo-frame name="react-simple">



</demo-frame>

Start it yourself with `bun run --cwd examples/react-simple dev`. [Quick start: React](https://skaile-ai.github.io/forge-editor/getting-started/quick-start-react) lists the dependencies, `vite.config.ts` and `index.css` an app needs; this chapter shows how the pieces map to what you already know.

## The context: `EditorContextProvider`

Vue has `app.use(createEditor())`. React has a context object that **you own**: `createEditorContext()` from the framework-free `@skaile/forge-editor/core` takes the same options as the Vue plugin (`backend`, `locale`, `ai`, …), and `EditorContextProvider` hands it to the tree. The provider never disposes the context; call `context.dispose()` yourself when the context outlives your app. The toolbar and menus render `i-lucide-*` names through `@iconify/react`, so the example preloads the icon collection and the page makes no request to the Iconify API:

```tsx [examples/react-simple/src/main.tsx]
import lucide from '@iconify-json/lucide/icons.json'
import { addCollection } from '@iconify/react'
import { createMemoryBackend } from '@skaile/forge-editor/adapters/memory'
import { createEditorContext } from '@skaile/forge-editor/core'
import { EditorContextProvider } from '@skaile/forge-editor/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './index.css'

// Preload the icon set: the page makes no request to the Iconify API.
addCollection(lucide)

// The host owns the context; the provider never disposes it. No data layer: documents, dropped files (as blob: URLs),
// mention users and tags, and settings live in the page.
const context = createEditorContext({
  backend: createMemoryBackend({
    users: [
      { id: 'u1', username: 'Ada Lovelace', color: '#7c3aed' },
      { id: 'u2', username: 'Alan Turing', color: '#0ea5e9' },
    ],
    tags: ['idee', 'entwurf', 'wichtig'],
  }),
})

createRoot(document.getElementById('app')!).render(
  <StrictMode>
    <EditorContextProvider context={context}>
      <App />
    </EditorContextProvider>
  </StrictMode>,
)
```

Notices of app-level models, such as the AI model profiles, go through the context's notifier. The simple editor raises none, so `createEditorContext()` is enough here. A host of features passes `createEditorContext({ notify: reactNotifier })` and mounts sonner's `Toaster` once (both from `@skaile/forge-editor/react`); components such as `DocumentTree` need the `Toaster` to be mounted.

## `EditorSurface` and `EditorToolbar`

The React `EditorSurface` takes the same kind of preset as the Vue one. `EditorToolbar` is again a sibling that works on the editor you pass it (from `onReady`), never on a global active editor, and it renders no groups without an editor. The preset follows chapter 3, with the same feature factories:

```tsx [examples/react-simple/src/App.tsx]
// Preset and metadata are read once at mount: module constants.
const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature<ComponentType<{ editor: Editor }>>({ palette: EditorCommandPalette }), mentionsFeature()] })
const metadata: EditorMetadata = {
  mentions: [
    { id: 'u1', label: 'Ada Lovelace', color: '#7c3aed' },
    { id: 'u2', label: 'Alan Turing' },
  ],
}
```

`coreContentFeature()`, `coreUiFeature()`, `mentionsFeature()` and the three integrations have no components and work in both layers. A feature with components has a React entry: `mediaFeature` from `@skaile/forge-editor/media/react`, `blockUiFeature` from `@skaile/forge-editor/block-ui/react`, and so on. Mixing a Vue feature into a React preset is a type error. Module constants matter more than in Vue: the preset, `metadata`, `abilities`, `extensions` and `commands` are read once at mount.

The surface itself differs from Vue in the prop names: `value` and `onChange` instead of `v-model`, `onReady` instead of `@ready`, and so on:

```tsx [examples/react-simple/src/App.tsx]
<EditorSurface
              preset={preset}
              metadata={metadata}
              commands={hostCommands}
              value={content}
              editable={editable}
              onReady={setEditor}
              onChange={onChange}
              onContentError={onContentError}
            />
```

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        preset
      </code>
    </td>
    
    <td>
      <code>
        EditorSurface
      </code>
      
       only: <code>
        defineEditor({ features })
      </code>
      
       (a <code>
        ReactPreset
      </code>
      
      ); fixed at mount, remount with <code>
        key
      </code>
      
       to change it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        value
      </code>
      
       / <code>
        onChange
      </code>
    </td>
    
    <td>
      TipTap JSON document and its edits. A new object replaces the document without an <code>
        onChange
      </code>
      
      ; JSON the editor emitted itself and the host hands back unchanged is ignored, so the round trip keeps caret and undo history.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        editable
      </code>
    </td>
    
    <td>
      Read-only when <code>
        false
      </code>
      
       (default <code>
        true
      </code>
      
      ); reactive.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        metadata
      </code>
    </td>
    
    <td>
      Host lists (<code>
        EditorMetadata
      </code>
      
      ); reactive, pass a constant.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        abilities
      </code>
      
      , <code>
        extensions
      </code>
      
      , <code>
        commands
      </code>
    </td>
    
    <td>
      As in Vue; read once at mount.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onReady
      </code>
      
      , <code>
        onMetadataCreate
      </code>
      
      , <code>
        onContentError
      </code>
    </td>
    
    <td>
      The Vue events <code>
        ready
      </code>
      
      , <code>
        metadataCreate
      </code>
      
       and <code>
        contentError
      </code>
      
       as callbacks.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        settingsUi
      </code>
    </td>
    
    <td>
      <code>
        'internal'
      </code>
      
       (default) mounts the settings modal inside the surface; <code>
        'external'
      </code>
      
       calls <code>
        onSettingsOpen(tab?)
      </code>
      
       and the host renders <code>
        EditorSettingsPanel
      </code>
      
       or <code>
        EditorSettingsModal
      </code>
      
      .
    </td>
  </tr>
</tbody>
</table>

## The one-liner: `SimpleEditor`

Without own features, `SimpleEditor` from `@skaile/forge-editor/react/simple` builds the preset itself (`core-content` and `core-ui`, plus the features in its `add` prop) on an `EditorSurface`. The ref exposes `{ editor, openSettings, preset }`:

```tsx
import { EditorToolbar } from '@skaile/forge-editor/react'
import { SimpleEditor } from '@skaile/forge-editor/react/simple'
import type { Editor } from '@tiptap/core'
import { useState } from 'react'

export function App() {
  const [editor, setEditor] = useState<Editor | null>(null)
  return (
    <>
      <EditorToolbar editor={editor ?? undefined} />
      <SimpleEditor onReady={setEditor} />
    </>
  )
}
```

## Settings in React

`useEditorSettings()` from `@skaile/forge-editor/react` reads the app's settings from the context: `{ settings, update, set, ready, synced }`, where `update(draft => { … })` mutates a copy and stores it and `set(value)` replaces it. Persistence is the same as in Vue: the `localStorage` key `tiptap-editor:settings`, optionally synced per account.

## Where the callout would go

There is no React callout in the repository: the Hinweis block of the examples is a Vue example, and the React sample says so („Hinweis-Blöcke gehören nicht zur Bibliothek; das Vue-Beispiel bringt einen eigenen mit.“). Porting it is small, because the node is already framework-free:

- `node.ts` from chapter 4 stays unchanged. It imports only `@tiptap/core` and `@skaile/forge-editor/core/schema`.
- The second file attaches the view: `Callout.extend({ addNodeView: () => ReactNodeViewRenderer(CalloutView) })` with `ReactNodeViewRenderer` from `@tiptap/react` and a React `CalloutView` built from `NodeViewWrapper` and `NodeViewContent`.
- Path A is the same: pass `extensions` (a module constant) and `commands` (a module function) to `EditorSurface` or `SimpleEditor`.
- Path B is the same: `calloutFeature()` fills no component slot, so it is an `EditorFeature<never>` and fits a React list as it is. Add `blockUiFeature()` from `@skaile/forge-editor/block-ui/react` for the hover actions.

## What you have now

The same editor on React: a context you own, an `EditorSurface` over a preset of the features from chapter 3, a toolbar, the `SimpleEditor` shortcut and the settings hook. You have finished the tutorial: from a simple editor to composed features, an own block, a server with collaboration and AI, on both surfaces.

## Next

- [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react) for the full catalog on React (`AdvancedEditor`, `composeEditor`, collaboration).
- [Exports](https://skaile-ai.github.io/forge-editor/reference/exports) for the inventory of every entry of the package.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
