-
Notifications
You must be signed in to change notification settings - Fork 2
Content and State
A NativeEditorDocumentHandle is the document's owner. It fixes the initial document, schema, editing policy, and limits at creation time. A mounted RichTextEditor binds to that same handle; a collaboration controller must bind to the same handle too.
Create a handle once with useMemo (or another lifetime owner) and destroy it on cleanup. Do not create one during every render.
import { useEffect, useMemo } from 'react';
import {
createNativeEditorDocumentHandle,
RichTextEditor,
} from '@apollohg/react-native-rich-text-editor';
export function DraftEditor() {
const documentHandle = useMemo(
() =>
createNativeEditorDocumentHandle({
initialization: { type: 'localHtml', html: '<p>Draft</p>' },
}),
[]
);
useEffect(() => () => documentHandle.destroy(), [documentHandle]);
return <RichTextEditor documentHandle={documentHandle} />;
}| Need | Recommended path |
|---|---|
| A locally edited draft whose state can stay in the engine | Initialize the handle with localEmpty, localHtml, or localJson; listen to change callbacks to save. |
| React state or another local store owns the current HTML | Pass controlled value and update it from onContentChange. |
| React state or another local store owns ProseMirror JSON | Pass controlled valueJSON and update it from onContentChangeJSON. |
| A non-visual document service needs reads, writes, or undo | Use useNativeEditorDocument with the shared handle. |
| Formatting or insertion acts on the current selection | Use a RichTextEditor ref. |
| A Yjs room owns the document | Follow Collaboration; do not add a second app-controlled valueJSON. |
Choose exactly one handle initialization:
{ initialization: { type: 'localEmpty' } }
{ initialization: { type: 'localHtml', html: '<p>Hello</p>' } }
{
initialization: {
type: 'localJson',
json: { type: 'doc', content: [{ type: 'paragraph' }] },
},
}The handle initialization establishes the first local document. It is not an editor prop and is not reapplied on rerender. When the mounted editor is controlled, the current content source has this precedence:
| Priority | Source |
|---|---|
| 1 |
value (controlled HTML) |
| 2 |
valueJSON (controlled ProseMirror JSON, only when value is absent) |
| 3 | The handle's localEmpty, localHtml, or localJson initialization |
Use localEmpty rather than { type: 'doc', content: [] } for empty initialization: defaultSchema requires one or more blocks, so that empty root is not valid localJson. An empty root supplied through controlled valueJSON is normalized to the handle schema's valid empty document. localJson, imperative setContentJson, and insertContentJson pass their JSON through; provide a schema-valid document or fragment for those paths.
Custom atom attributes are ordinary document state. Calling an atom component's updateAttrs() creates a revision-guarded document transaction, so the new attributes flow through JSON/HTML output, change callbacks, undo, persistence, and Yjs collaboration. See Custom Atom Nodes.
Uncontrolled does not mean unobservable. Initialize the handle once, then use callbacks to persist output without feeding it straight back as a controlled value.
import { useEffect, useMemo } from 'react';
import {
createNativeEditorDocumentHandle,
RichTextEditor,
type DocumentJSON,
} from '@apollohg/react-native-rich-text-editor';
export function UncontrolledDraft({ save }: { save: (json: DocumentJSON) => void }) {
const documentHandle = useMemo(
() => createNativeEditorDocumentHandle({ initialization: { type: 'localEmpty' } }),
[]
);
useEffect(() => () => documentHandle.destroy(), [documentHandle]);
return (
<RichTextEditor
documentHandle={documentHandle}
onContentChangeJSON={save}
/>
);
}Use this path for ordinary compose, notes, and draft screens where the native/Rust session is the working copy and your app saves snapshots at a cadence it owns.
The editor has no generic onChange prop. Use onContentChange for HTML and onContentChangeJSON for JSON. Pair each callback with only its matching controlled prop.
import { useEffect, useMemo, useState } from 'react';
import {
createNativeEditorDocumentHandle,
RichTextEditor,
} from '@apollohg/react-native-rich-text-editor';
export function ControlledHtmlEditor() {
const [html, setHtml] = useState('<p>Hello</p>');
const documentHandle = useMemo(
() => createNativeEditorDocumentHandle({ initialization: { type: 'localEmpty' } }),
[]
);
useEffect(() => () => documentHandle.destroy(), [documentHandle]);
return (
<RichTextEditor
documentHandle={documentHandle}
value={html}
onContentChange={setHtml}
/>
);
}For JSON, add valueJSONRevision when a parent may recreate equivalent object graphs. A stable revision lets the component skip redundant serialization work.
<RichTextEditor
documentHandle={documentHandle}
valueJSON={documentJson}
valueJSONRevision={documentVersion}
onContentChangeJSON={setDocumentJson}
/>;valueJSONUpdateMode describes how external JSON replacements affect local undo history:
| Mode | Behavior |
|---|---|
"replace" (default) |
Replaces the document as an undoable boundary and retains existing history. |
"reset" |
Authoritatively replaces the document, discards pending native input/composition, and clears history. |
Use replace for app-driven edits that should remain undoable. Use reset for authoritative loads such as switching records or restoring a draft where prior undo history is no longer meaningful. The mode applies to controlled valueJSON; imperative whole-document setters preserve history, while clearContent() is a reset-style clear.
A reset is applied even when clearing an already-empty engine document: pending native text must still be discarded. On iOS and Android, a pending keyboard or external composition cannot restore the old content after an authoritative reset. Ordinary replace operations retain their composition-reconciliation behavior.
The binding recognizes a delayed controlled HTML or JSON echo of an earlier native callback. If the native document has already advanced, that stale echo is ignored instead of rolling back the newer edit or moving the caret.
Undo history belongs to the document handle. Views and useNativeEditorDocument bindings attached to the same handle use the same history. Call editorRef.current?.undo() and editorRef.current?.redo(), or use the corresponding methods from useNativeEditorDocument.
One undo reverses one history group, which can contain more than one edit. Consecutive typing can be grouped; content-changing paste and cut actions create their own boundaries, separate from adjacent typing.
The following behaviour applies on both iOS and Android:
| Action | Undo behaviour |
|---|---|
| Copy | Does not change document content or add an undo entry. |
| Paste rich content | The entire insertion or selection replacement is one undo step, including its marks, nodes, and atom attributes. This also applies to supported content imported from external rich text sources. |
| Paste plain text | The entire insertion or selection replacement is one undo step, including multiple lines. This applies to pasteMode="plainText" and explicit Paste as Plain Text. |
| Cut | Copies the selection before deleting it. The deletion is one undo step; undo restores the deleted document content. |
| Disabled paste or a paste that makes no document change | Does not add a paste undo entry. |
For example, select an existing paragraph and paste two formatted paragraphs over it. One undo restores the original paragraph; one redo reapplies the pasted content. You do not need separate undo actions for deleting the selection and inserting its replacement. The same rule applies when pasting into a list item or blockquote.
Undo and redo change the document; they do not restore previous system clipboard contents. If a clipboard interaction first commits pending keyboard or external text composition, that composition is a separate edit from the clipboard action.
Use onHistoryStateChange to receive { canUndo, canRedo } and enable your controls. The editor ref also exposes canUndo() and canRedo() for imperative checks. The default toolbar includes undo and redo; see Toolbar Setup for custom controls.
Whole-document setters and controlled valueJSONUpdateMode="replace" retain history and create an undoable replacement. clearContent() and valueJSONUpdateMode="reset" clear history. See the controlled-content rules above before using a reset for an app-driven update. Remote collaboration updates do not become local undo entries.
onContentChange and onContentChangeJSON emit the current engine snapshot after a document change. The editor also exposes focused state callbacks for UI outside the native view:
| Callback | Use it for |
|---|---|
onSelectionChange(selection) |
Selection-aware app UI. Positions are engine document positions. |
onActiveStateChange(activeState) |
Enabling formatting controls and reading an active link's attributes. |
onHistoryStateChange(historyState) |
Enabling undo and redo controls. |
onFocus / onBlur
|
Screen-level focus behavior. |
onLocalCommit |
Application notification after a successful local mutation; native collaboration transport does not depend on it. |
EditorUpdate is the exported snapshot shape behind native render updates. It contains renderElements, selection, activeState, and historyState (plus optional render-block patch/document-version fields). Application UI normally consumes the callbacks above rather than native render elements directly.
The important state types are:
-
Selection: a text selection with optionalanchor/head, a node selection with optionalpos, or{ type: 'all' }. -
ActiveState: maps of active marks, mark attributes, nodes, and commands, plusallowedMarksandinsertableNodes. For example,activeState.markAttrs.link.hrefis the active link target. -
HistoryState:{ canUndo, canRedo }. -
DocumentJSON: intentionally schema-dependent ProseMirror JSON.
See Types and Events Reference for their complete definitions.
useNativeEditorDocument is the headless retained-document binding for an existing handle. It never creates or destroys that handle. It exposes readiness, document state, revision and origin, HTML/JSON/text getters, setContent, setContentJson, clearContent, undo/redo, and refresh().
documentOrigin is nativeView, jsApi, remoteCollaboration, history, restore, or import. It is null while the document is not ready.
Use it when a screen or service needs to read or replace a shared document without going through a view ref. The component uses the same binding internally, so pass its callbacks to one owner to avoid treating two callback streams as independent sources of truth.
import { useEffect, useMemo } from 'react';
import {
createNativeEditorDocumentHandle,
useNativeEditorDocument,
} from '@apollohg/react-native-rich-text-editor';
export function DocumentStatus() {
const handle = useMemo(
() => createNativeEditorDocumentHandle({ initialization: { type: 'localEmpty' } }),
[]
);
const document = useNativeEditorDocument({ handle });
useEffect(() => () => handle.destroy(), [handle]);
return document.isReady ? document.getTextContent() : 'Loading…';
}A room-backed handle can remain not ready while it awaits the server document. Its getters return empty values until the room is accepted and promoted. This is intentional; do not substitute an unrelated local document while waiting.
Changing documentHandle from one ready handle to another keeps the native editor mounted and replaces its document without a loading frame. If the editor was focused, it preserves the keyboard and clamps the existing caret or selection into the replacement document. This is suitable for switching ready drafts in one editing surface.
An AwaitRemote room is different: it remains loading until accepted room content arrives and does not inherit focus from the prior handle.
Use a RichTextEditorRef for operations tied to the mounted editor and its current selection:
import { useRef } from 'react';
import { Button } from 'react-native';
import {
RichTextEditor,
type NativeEditorDocumentHandle,
type RichTextEditorRef,
} from '@apollohg/react-native-rich-text-editor';
export function EditorActions({
documentHandle,
}: {
documentHandle: NativeEditorDocumentHandle;
}) {
const editorRef = useRef<RichTextEditorRef>(null);
return (
<>
<Button title="Bold" onPress={() => editorRef.current?.toggleMark('bold')} />
<RichTextEditor ref={editorRef} documentHandle={documentHandle} />
</>
);
}Call the ref from a button or another event handler: focus(), toggleMark('bold'), setLink(href), insertImage(src, attrs), and clearContent() are common examples. The full ref provides focus/blur, mark/link/blockquote/heading/list commands, list indent/outdent, node/text/HTML/JSON/image insertion, whole-document HTML/JSON setters, getters, caret geometry, and undo/redo. See RichTextEditor Reference for the complete method list and Links and Images for link/image workflows.
React Native Rich Text Editor · Documentation · Migration Guide