Skip to content

Architecture

Jayden Smith edited this page Sep 6, 2026 · 6 revisions

Architecture

React Native Rich Text Editor has one document-semantic core and two native UI implementations. The package-root API composes those layers; it does not expose native implementation symbols as application API.

Boundary map

Boundary Responsibility Main source area
React Native package Public components, document-handle creation, controlled binding, themes, toolbar configuration, schema/addon/atom helpers, mounted atom components, and typed boundary errors. src
Native module and editor view Expo module methods, handle registry, platform text input, IME/composition, selection conversion, native toolbar, image loading, and applying engine render snapshots. android/src/main/java/com/apollohg/editor and ios
Rust core Schema validation, document model, HTML/JSON conversion, transactions, history, selection normalization, render preparation, collaboration runtime, snapshots, and resource admission. rust/editor-core/src
Native viewer Fabric view manager/component, prepared-layout cache and measurement, drawing, image loading, accessibility, and link/mention interactions. android/.../viewer, ios/Viewer, common/cpp/.../PreparedProseViewer

React Native and document sessions

createNativeEditorDocumentHandle serializes the creation config, creates one native session, and returns the nominal handle. RichTextEditor, useNativeEditorDocument, and useYjsCollaboration all bind to that one session by editorId.

The React layer owns:

  • Component composition and React lifecycle.
  • Controlled HTML/JSON applies and callback delivery.
  • Ref-command routing for an interactive editor.
  • Serialization of schema, theme, toolbar, addon, image-policy, and remote-selection inputs.
  • Collection and placement of consumer React atom components from authoritative render blocks.
  • Boundary normalization into typed errors with decimal-string revision fields.

The handle owns the engine session lifetime. A component does not set initialization or swap schemas; a viewer is separate and creates no editable session. The document flow and lifetime rules are in Content and State.

Native editor boundary

Both platform modules register NativeEditor and expose the v2 create, destroy, state/content, mutation, render-update, collaboration, and snapshot operations. The React bridge sends canonical JSON envelopes and native modules route them to generated UniFFI bindings for the Rust core.

Native editor views own platform-specific interaction:

  • Android: an Expo view hosts RichTextEditorView. Its owned EditorTextSurface retains one editable buffer and serves native input through an InputConnection; EditorDocumentLayout shapes paragraph fragments with global document offsets. Caret, selection, hit testing, block boxes, images, and scrolling use that layout. Custom React atoms are real children of the native scrolling content, with measured heights participating in document layout.
  • iOS: an Expo view hosts the rich text view, input accessory toolbar, position bridge, render bridge, and shared image pipeline.
  • Both platforms apply Rust-authorized render updates and keep native text/IME composition synchronized with the session. The native view is not a second document authority.

Native interaction commits update the shared session. JavaScript-driven engine updates produce an immutable render snapshot for the bound view. A room in AwaitRemote remains loading rather than rendering unrelated local fallback content.

Custom atoms cross this boundary as block void nodes. Rust owns their schema, positions, declared attributes, HTML/JSON conversion, transactions, history, and collaboration state. The native editor reserves measured positions in its text layout, while React mounts the registered component as a native child at each position. updateAttrs() routes back through a revision-guarded engine command; the component is presentation, not a second document authority. See Custom Atom Nodes.

Rust core and bridge contract

rust/editor-core is the semantic authority. Its schema, model, selection, serialization, transform, render, Yjs engine, collaboration runtime, document API, session registry, and ffi_v2 modules are compiled into the editor-core native library.

The v2 FFI is the only shipped editor surface. Generated Swift and Kotlin bindings use the UniFFI library; the packaging scripts verify the expected editor_v2 symbols and reject legacy editor/collaboration symbols. Native modules normalize result envelopes before they reach React Native.

Keep a bug in the layer that owns its invariant:

Symptom Owning area
Invalid schema, document shape, transform, history, revision, snapshot lineage, or Yjs state Rust core / v2 contract
Keyboard, text composition, cursor geometry, font/layout, image decode, platform accessibility iOS or Android implementation
Public props, lifecycle composition, controlled React flow, serializable configuration React Native package

Rendering and viewer boundary

The editor asks Rust for prepared render blocks and state. Platform editor views turn those blocks into editable native content, selections, toolbar state, and image/mention presentation.

RichTextViewer takes a different path. Its Fabric component accepts one HTML or JSON source plus serialized schema/theme/configuration, compiles/prepares native layout, measures against its width, draws the result, and publishes native interactions. The viewer has its own prepared-layout registries and caches; it never binds an editable document handle.

Both editor and viewer can mount registered React atom components. The viewer combines prepared native prose with measured React atom hosts; optional onUpdateAtomAttrs proposals let the application update its controlled source. It creates no editable session or undo history. Missing renderers retain the native fallback. See Custom Atom Nodes.

The C++ PreparedProseViewer shadow-node support participates in Fabric measurement, while Android and iOS retain platform-specific layout/drawing/accessibility code. This is why the viewer requires the New Architecture and a finite positive width. See Viewer.

Styles and optional providers

EditorStyleSheet.create and theme normalization validate a typed element map before native rendering. Text properties inherit through blocks; each element owns its background, spacing, and borders. Android uses physical per-side block geometry, including RTL; justification requires API 26+, with normal alignment on API 24/25. Layout rebuilds still scan the editable buffer while reusing unchanged paragraph fragments.

Addons are an ordered array of versioned capability descriptors. The separately installed code-highlighting package registers a syntect native provider. The core editor does not bundle syntect or its grammars. Highlighting changes presentation without changing text, positions, history, or collaboration content. See Addons and Code Syntax Highlighting.

Collaboration boundary

For a room handle, Rust owns the Yjs document, awareness clocks, snapshot validation, bounded queues, and retry eligibility. The native layer owns physical WebSocket lifecycle and forwards transport events. JavaScript projects status/peers into useYjsCollaboration and may provide an attempt-scoped protocol adapter for app-specific authentication.

The server is outside this repository's package boundary. It must implement the Yjs sync and awareness protocol, authentication/authorization, and durable persistence. Application code must not create a competing JavaScript WebSocket, retry loop, or second document store. See Collaboration.

Development artifacts

Rust source produces generated Swift/Kotlin bindings, iOS XCFramework slices, and Android shared libraries. The package's build and publish scripts treat these as generated outputs and validate their ABI/package inclusion. Native source changes need platform build validation; Rust interface changes require regenerating bindings and both platform artifacts. See Development Workflow.

Related pages

Clone this wiki locally