superdoc/ui/react is sugar over a controller. If you are not using React, talk to the controller directly. The hooks just call its methods on your behalf.
When to use this page
The controller exposes the same surface the React hooks consume. Domain handles, scope-based lifecycle, value-shaped observers. No framework primitives.
Install
Create the controller
createSuperDocUI({ superdoc }) runs once per editor mount and returns a controller. Hand it the SuperDoc instance.
ui is null-safe to keep around until your app tears down. Call ui.destroy() on unmount.
Bind state with observe
Domain handles emit throughobserve(snapshot => ...). The listener fires once synchronously with the current snapshot, then again on every change. Returns an unsubscribe.
ui.toolbar.observe, ui.selection.observe, ui.trackChanges.observe, ui.document.observe. Per-command state binds the same way: ui.commands.bold.observe(state => ...).
A wrapped subscribe(({ snapshot }) => ...) form is also exported. Either works; pick one and stay consistent.
Group teardown with createScope
WithoutuseEffect cleanup, you have to track every unsubscribe yourself. ui.createScope() does that for you.
ui.destroy() cascades into every live scope, so a typical app needs only one teardown call:
Register custom commands
scope.register(...) is the same as ui.commands.register(...) but the scope auto-unregisters on tear-down. The registration is reachable through the same ui.commands.<id> and ui.commands.get(id) paths as built-ins.
Validate config-driven command ids
If your toolbar reads ids from a config file or feature flag, validate at startup.ui.commands.has(id) is the cheap check; ui.commands.require(id) throws on unknown ids at trusted dispatch sites.
BUILT_IN_COMMAND_IDS is the readonly list of every valid built-in id. PublicToolbarItemId is the matching type.
Tiny skeleton
The whole picture in one file:What ships
See the API reference for full signatures.
Common pitfalls
Forgetting to call ui.destroy()
Forgetting to call ui.destroy()
Without
ui.destroy(), internal listeners and any live scope keep running after your app unmounts. Hot-reload sessions accumulate dead controllers. Always call it on unload and on every framework-specific destroy hook (onScopeDispose in Vue, onDestroy in Svelte, DestroyRef in Angular).Reading the live selection at submit time
Reading the live selection at submit time
A composer that reads
ui.selection.getSnapshot() at submit time will see null if the user typed in a textarea between opening the composer and pressing Send. Capture the selection at composer-open with ui.selection.capture() and pass the snapshot into ui.comments.createFromCapture(capture, { text }).Built-in UI overlapping with yours
Built-in UI overlapping with yours
Pass
modules: { comments: false } to new SuperDoc(...) to disable the built-in comment bubble. Same shape for tracked changes. Document-level features (DOCX import/export, comments round-trip) keep working.
