npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eclosion-tech/react-native-yjs-text

v0.1.0

Published

Native React Native rich text editor backed by Y.Text — no WebView, no contenteditable

Readme

@eclosion-tech/react-native-yjs-text

Native React Native rich text editor backed by Y.Text. No WebView, no contenteditable, no DOM.

Built as an Expo Module — works in any Expo SDK 52+ project (and in bare RN projects via expo-modules-core).

Status: v0.1.0 — alpha. The TypeScript layer, Swift iOS view, and Kotlin Android view all compile cleanly. Both example apps (Expo-managed in example/ and bare RN in example-bare/) link the library and produce a working .app / .apk. A 50-case Jest suite covers the bridge, schema, and editor hook. The remaining v0.1 work is on-device behavioural verification (typing, mark toggling, caret stability under remote edits).


Why this exists

Every existing rich-text editor in React Native today wraps a browser editor in a WebView: react-native-pell-rich-editor, @10play/tentap-editor, react-native-cn-quill. They all pay the WebView tax — worse performance, broken keyboard behaviour, gesture / scroll conflicts, accessibility regressions, platform-specific bugs that don't reproduce in browsers.

Y.Text is already a platform-independent rich-text primitive — a sequence of characters with arbitrary formatting attributes. That maps cleanly onto contenteditable (via y-prosemirror on the web) and onto NSAttributedString / Spannable on native mobile. This library is the native-view side of that binding.

See SPEC.md for the full design rationale, non-goals, and versioning roadmap.

What ships in v0.1

  • YTextInput — editable view backed by UITextView (iOS) / AppCompatEditText (Android)
  • YTextRenderer — read-only renderer that uses RN's native <Text> with attributed spans (no UITextView instance, much cheaper)
  • useYTextEditor — imperative editor hook (toggleMark, setMark, removeMark, insertText, deleteRange, focus, blur, getSelection, setSelection, marksAtSelection)
  • defaultSchemabold, italic, underline, strike, code, link (matching the y-prosemirror mark convention so the same Y.Text content edits identically on web)
  • Bidirectional sync between user edits and Y.Text mutations, with Y.RelativePosition-based caret preservation across remote insertions

Install

Expo (managed or prebuild)

npx expo install @eclosion-tech/react-native-yjs-text yjs isomorphic-webcrypto
npx expo prebuild         # if you don't already have native projects
npx expo run:ios          # or run:android

Expo Go can't load the library (native code); you need a dev build.

Why isomorphic-webcrypto? yjs's underlying utility lib lib0 generates random doc / client IDs via crypto.getRandomValues. React Native doesn't expose webcrypto by default, so lib0's RN entry-point hard-requires isomorphic-webcrypto and expects the consumer to install it. (You can alternatively install react-native-get-random-values and import 'react-native-get-random-values' before the first yjs import — then lib0 skips its webcrypto shim and uses the polyfilled native one. Either works; pick whichever your app already has.)

Bare React Native (RNCLI scaffold, brownfield, etc.)

The library uses the Expo Modules API for its native side, so a bare RN host needs Expo Modules autolinking. This does not turn your app into "an Expo app" — no app shell, no Expo Go runtime, no expo-router. You're only opting into native autolinking.

npm install @eclosion-tech/react-native-yjs-text yjs expo isomorphic-webcrypto
# expo brings in expo-modules-core + the autolinking scripts; nothing else
# isomorphic-webcrypto is required by yjs's lib0 dep — see the Expo section
# above for the alternative `react-native-get-random-values` route

Then three small wiring changes:

  1. ios/Podfile — load Expo's autolinking + call use_expo_modules!:

    require File.join(
      File.dirname(`node --print "require.resolve('expo/package.json')"`),
      "scripts/autolinking"
    )
    
    platform :ios, '16.4'   # expo-modules-core requires 16.4+
    
    target 'YourApp' do
      use_expo_modules!
      config = use_native_modules!
      # ...rest of the standard RN Podfile target...
    end
  2. ios/YourApp/AppDelegate.swift — subclass Expo's app-delegate / factory wrappers so Expo Modules register at boot:

    internal import Expo   // matches the access level of the generated
                           // ExpoModulesProvider.swift
    import React
    import ReactAppDependencyProvider
    
    @main
    class AppDelegate: ExpoAppDelegate {
      var window: UIWindow?
      var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
      var reactNativeFactory: RCTReactNativeFactory?
    
      public override func application(/* ... */) -> Bool {
        let delegate = ReactNativeDelegate()
        let factory = ExpoReactNativeFactory(delegate: delegate)
        delegate.dependencyProvider = RCTAppDependencyProvider()
        reactNativeDelegate = delegate
        reactNativeFactory = factory
        window = UIWindow(frame: UIScreen.main.bounds)
        factory.startReactNative(withModuleName: "YourApp",
                                  in: window, launchOptions: launchOptions)
        return super.application(application,
                                 didFinishLaunchingWithOptions: launchOptions)
      }
    }
    
    class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { /* ... */ }
  3. android/settings.gradle — pull in the Expo autolinking gradle plugin and call expoAutolinking.useExpoModules(). android/app/ src/main/java/.../MainApplication.kt — replace DefaultReactHost.getDefaultReactHost(...) with ExpoReactHostFactory.getDefaultReactHost(...) and add ApplicationLifecycleDispatcher.onApplicationCreate(this) / onConfigurationChanged(this, newConfig).

The complete working set of changes for a fresh @react-native-community/ cli init project lives in example-bare/ — copy from there if you'd rather diff against a known-good reference than apply the snippets above by hand.

Usage

import {
  YTextInput,
  YTextRenderer,
  defaultSchema,
  useYTextEditor,
} from '@eclosion-tech/react-native-yjs-text';
import * as Y from 'yjs';

const doc = new Y.Doc();
const yText = doc.getText('content');

function Editor() {
  const editor = useYTextEditor(yText, defaultSchema);
  return (
    <>
      <Toolbar editor={editor} />
      <YTextInput
        yText={yText}
        schema={defaultSchema}
        style={{ fontSize: 16, color: '#111' }}
        placeholder="Start typing…"
      />
      <YTextRenderer yText={yText} schema={defaultSchema} />
    </>
  );
}

function Toolbar({ editor }) {
  return (
    <View>
      <Button onPress={() => editor.toggleMark('bold')} title="B" />
      <Button onPress={() => editor.toggleMark('italic')} title="I" />
      <Button
        onPress={() => editor.setMark('link', { href: 'https://yjs.dev' })}
        title="Link"
      />
    </View>
  );
}

The library does not own the Y.Doc / Y.Text — the consumer creates, syncs (via y-websocket, y-indexeddb, a custom provider, whatever), and disposes of them. The component subscribes to a passed-in Y.Text and edits it in place.

See example/App.tsx for a full demo that exercises every default mark, programmatic mutations, and YTextRenderer.

Schemas

A schema declares which marks the editor accepts. Marks not in the schema are dropped on insert — this is what makes the editor safe for AI-generated content. Custom marks are declared by extending defaultSchema:

import { defaultSchema, type Schema } from '@eclosion-tech/react-native-yjs-text';

const schema: Schema = {
  marks: {
    ...defaultSchema.marks,
    'pear.mention': {
      attrs: { userId: { type: 'string', required: true } },
      renderStyle: { color: '#0066cc', backgroundColor: '#e6f0ff' },
      onTap: (attrs) => console.log('mention tapped', attrs.userId),
    },
  },
};

Only a subset of RN's TextStyle survives the bridge into native rendering: fontWeight, fontStyle, fontFamily, fontSize, color, backgroundColor, textDecorationLine. Keys outside that list are silently dropped (a deliberate cross-platform-safety choice — see RENDERABLE_TEXT_STYLE_KEYS in src/schema.ts).

Building & running

This repo is a pnpm workspace with three members:

  • . — the library
  • example/ — Expo SDK 56 demo (uses expo run:ios / expo run:android)
  • example-bare/ — vanilla @react-native-community/cli demo (no Expo runtime around the host app, only Expo Modules autolinking on the native side)
# From the repo root
pnpm install              # installs all three workspace members
pnpm prepare              # builds the library's TS into `build/`
pnpm test                 # runs the Jest suite (50 tests, ~1s)

# Expo example
cd example
npx pod-install           # or: cd ios && pod install
npx expo run:ios          # or: npx expo run:android

# Bare RN example
cd example-bare
(cd ios && pod install)
npx react-native run-ios  # or: npx react-native run-android

The bare example doubles as the canonical integration test for non-Expo hosts; if it boots, your @react-native-community/cli init-shaped project will too.

Architecture

┌─────────────────────────────────────────┐
│ Consumer application                    │
│   - manages the Y.Doc / Y.Text          │
│   - builds toolbar / slash menu UI      │
│   - composes multiple YTextInputs into  │
│     its own block model                 │
└──────────────┬──────────────────────────┘
               │ <YTextInput yText={...} schema={...} />
               │
┌──────────────▼──────────────────────────┐
│ react-native-yjs-text (TS layer)        │
│   - YTextInput / YTextRenderer          │
│   - useYTextEditor command API          │
│   - schema definitions / mark validation│
│   - Y.Text ↔ runs ↔ native event bridge │
└──────────────┬──────────────────────────┘
               │ Expo Modules (Fabric / TurboModules)
               │
┌──────────────▼──────────┬───────────────┐
│ iOS native              │ Android native│
│ - YjsTextView (ExpoView │ - YjsTextView │
│   + UITextView subview) │   (ExpoView + │
│ - NSAttributedString    │   AppCompatEditText)
│ - UITextViewDelegate    │ - SpannableStringBuilder
│   shouldChangeTextIn    │ - TextWatcher │
└─────────────────────────┴───────────────┘

Known gaps in v0.1

Per the spec (SPEC.md § Versioning roadmap):

  • IME composition for CJK / Korean / IMEs with multi-stage input. We hand the composition string straight through to Y.Text on every keystroke in the composition session, which collaborators see as a flurry of character-by-character inserts and deletes instead of one atomic composition commit. A "compose locally, transact on commit" path lands in v0.2 once we add UITextInput.markedTextRange / Android InputConnection.setComposingText handling.
  • Paste from system clipboard loses formatting — content is inserted as plain runs.
  • Hardware-keyboard shortcuts (⌘B / Ctrl+B etc.) are not bound by default; consumers can wire their own via the imperative editor API.
  • Bridge traffic goes through the standard Expo Modules ABI on every edit. v0.2 will move the typing-hot-path event (onContentChange) to a JSI direct call to skip JSON ser/de per keystroke.
  • No first-party block model. Each YTextInput edits one inline region; the consumer composes them into paragraphs / headings / lists. See SPEC.md for why this is by design.
  • Selection highlight is visually masked by backgroundColor marks. Both UITextView (iOS) and EditText (Android) draw NSAttributedString. backgroundColor / BackgroundColorSpan on top of the system selection highlight, so selecting text that already has, e.g., the default code mark applied makes the selection rectangle invisible inside the marked range. The selection is still active functionally — toolbar toggles and keyboard actions work as normal — only the visual rectangle is occluded. iOS users still see the two grab-handle circles. Mirrors how Mobile Safari, iOS Notes, and most platform editors render the same situation.

License

MIT. See LICENSE.