# React custom UI setup

> Bind React controls to the same public SuperDoc UI controller.



`superdoc/ui/react` provides a React provider and hooks over the framework-neutral controller. Use it when React should render your toolbar or panels while SuperDoc continues to own the document canvas and editing behavior.

This page builds a hybrid interface: React renders the toolbar, and the remaining built-in surfaces stay at their defaults because the example passes no `ui`. Add `ui: false` when the application should render every surface, or turn off individual keys as you replace them. [Choose an interface](/editor/ui/choose-an-interface) covers the three modes.

Complete [Custom UI controller setup](/editor/custom-ui/controller-setup) first if the controller lifecycle is new to you.

## Install the public package [#install-the-public-package]

Use the same `superdoc` package for the Editor, controller, and React binding:

```bash
pnpm add superdoc@2 react react-dom
```

The `superdoc/ui/react` subpath is part of `superdoc`. Do not install or import the removed v1 headless-toolbar packages.

## Bind the Editor to the provider [#bind-the-editor-to-the-provider]

This example loads `contract.docx` automatically and adds four application-owned controls: undo, bold, document mode, and fit-to-width.

```tsx
import { useEffect, useRef } from 'react';
import { SuperDoc } from 'superdoc';
import { SuperDocUIProvider, useSetSuperDoc, useSuperDocCommand, useSuperDocUI } from 'superdoc/ui/react';
import 'superdoc/style.css';

export function App() {
  return (
    <SuperDocUIProvider>
      <CustomToolbar />
      <Editor />
    </SuperDocUIProvider>
  );
}

function CustomToolbar() {
  return (
    <div aria-label='Document controls' role='toolbar'>
      <CommandButton id='undo' label='Undo' />
      <CommandButton id='bold' label='Bold' toggle />
      <DocumentModeSelect />
      <CommandButton id='zoom-fit-width' label='Fit width' />
    </div>
  );
}

function CommandButton({ id, label, toggle = false }: { id: string; label: string; toggle?: boolean }) {
  const ui = useSuperDocUI();
  const state = useSuperDocCommand(id);

  return (
    <button
      aria-pressed={toggle ? state.active : undefined}
      disabled={!state.enabled}
      onClick={() => void ui?.commands.executeAsync(id)}
      title={state.reason ?? label}
      type='button'
    >
      {label}
    </button>
  );
}

function DocumentModeSelect() {
  const ui = useSuperDocUI();
  const state = useSuperDocCommand('document-mode');
  const mode = typeof state.value === 'string' ? state.value : 'editing';

  return (
    <select
      aria-label='Document mode'
      disabled={!state.enabled}
      onChange={(event) => void ui?.commands.executeAsync('document-mode', event.currentTarget.value)}
      value={mode}
    >
      <option value='editing'>Editing</option>
      <option value='suggesting'>Suggesting</option>
      <option value='viewing'>Viewing</option>
    </select>
  );
}

function Editor() {
  const setSuperDoc = useSetSuperDoc();
  const editorRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!editorRef.current) return;

    const superdoc = new SuperDoc({
      selector: editorRef.current,
      document: '/contract.docx',
      onReady: ({ superdoc: readySuperDoc }) => setSuperDoc(readySuperDoc),
    });

    return () => superdoc.destroy();
  }, [setSuperDoc]);

  return <div ref={editorRef} style={{ height: '70vh' }} />;
}

```

Copy the [tracked-changes fixture](/fixtures/tracked-changes.docx) to your app's public directory as `contract.docx`, or update the document URL.

`SuperDocUIProvider` publishes the bound Editor's own controller, `superdoc.ui`. For a real `SuperDoc` it does not create one. `useSetSuperDoc()` returns the stable callback that binds the ready `SuperDoc` instance.

The React component still owns the Editor instance. Destroy it in the effect cleanup. The provider does not destroy an Editor-owned controller: `superdoc.destroy()` does that, so unmounting the provider or binding a different Editor leaves the controller intact for anything else reading it.

## Read command state through hooks [#read-command-state-through-hooks]

`useSuperDocCommand(id)` subscribes the component to one command. Before the Editor is ready, it returns a safe disabled state. After binding, it updates when the selection, document mode, or command availability changes.

Use the returned values directly:

* `enabled` controls whether the action can run.
* `active` drives toggle state such as Bold.
* `value` carries the current document mode or zoom value when the command provides one.
* `reason` explains why a known command cannot run.

Do not mirror these values into another React state object. The hook is already the reactive source of truth.

## Keep one controller per Editor [#keep-one-controller-per-editor]

Do not call `createSuperDocUI()` inside a component that already uses `SuperDocUIProvider`. That factory builds a second, independently owned controller; the provider publishes the Editor's own `superdoc.ui` when `useSetSuperDoc()` binds it.

You cannot call `ui.destroy()` from a component either: the hooks return the borrowed handle, which omits the method. The controller belongs to the Editor, and destroying it would leave the built-in toolbar and every other consumer of that instance reading dead state.

> **Binding something other than a SuperDoc instance (note)**
>
> The provider accepts any structural host, and `ui` is optional on that contract, so a custom adapter or a test double
> can satisfy it without carrying a controller. Only in that case does the provider build one, and then it owns that
> one: it destroys it when you bind a different host and again on unmount. Do not hold on to a controller the provider
> created for you. A real `SuperDoc` always carries its own, so this fallback never applies to the code above.


Place controls that use the hooks inside the provider. A hook outside the provider throws an explicit error instead of silently creating an unrelated controller.

Verify the result by selecting text in the DOCX. Bold should enable and reflect the current formatting. Undo should follow document history. Mode and fit-to-width should control the same mounted Editor.

Continue with [Custom UI overview](/editor/custom-ui/overview), or compare this approach with the [built-in toolbar](/editor/built-in-ui/configure-the-toolbar).
