docs/plans/2026-05-23-slate-v2-react-prosemirror-steals-ralplan.md
Steal three things from ../react-prosemirror:
Do not steal its core architecture. Its custom EditorView, ViewDesc, react-reconciler, and ProseMirror position-key plugin solve ProseMirror/React impedance. Slate v2 should keep its own runtime: one editor, many root views, path/runtime-id operations, root-scoped selectors, and browser-owned selection import/export.
Status: scoped planning answer complete. This lane records what to steal and what to reject; it does not claim an implementation, fixed issue, or Slate v2 behavior change.
../react-prosemirror scan into actionable Slate v2 architecture decisions.react-reconciler, replacing Slate v2 runtime/view architecture, changing data model identity, claiming fixed issues.ralph run.Principles:
Top drivers:
Viable options:
| Option | Pros | Cons | Verdict |
|---|---|---|---|
Copy react-prosemirror architecture | strong React/PM integration; proven redraw tests | couples to PM view internals and react-reconciler; wrong identity model for Slate | reject |
| Steal only hook/effect/proof mechanisms | improves Slate React DX/perf without changing the core model | needs careful API naming and test coverage | choose |
| Do nothing | avoids scope | leaves app examples to keep writing focus/selection hacks | reject |
Chosen option: add Slate-owned post-commit view effects and stable editor command callbacks, then copy the redraw/composition test philosophy.
../react-prosemirror/README.md:104-123: identifies the render-phase mismatch; React components can see newer state than the view, so dispatching or reading view methods during render is unsafe.../react-prosemirror/README.md:215-220: view methods like coordsFromPos must run outside render after DOM is current; exposes useEditorEffect and useEditorEventCallback.../react-prosemirror/src/hooks/useEditor.ts:126-145: updates the view in render as pure state, then commits pending effects in a layout effect.../react-prosemirror/src/hooks/useEditorEffect.ts:11-24: hook runs after EditorView has latest state and decorations.../react-prosemirror/src/hooks/useEditorEventCallback.ts:24-53: returns a stable handler that calls the latest mounted view.../react-prosemirror/src/components/LayoutGroup.tsx:13-19: groups descendant layout effects so editor effects run after descendant layout effects.../react-prosemirror/src/ReactEditorView.ts:69-79: makes prop/state updates pure and uses a React-managed document view.../react-prosemirror/src/ReactEditorView.ts:122-130: pauses React-driven selection/DOM updates during composition.../react-prosemirror/src/plugins/reactKeys.ts:21-27 and :47-91: maps stable node keys through transactions; freezes during composition at :95-101.../react-prosemirror/src/decorations/viewDecorations.tsx:179-230: memoizes equivalent decoration sources per view to preserve identity.../react-prosemirror/src/components/__tests__/ProseMirror.draw.test.tsx: tests that unrelated siblings are not redrawn after edits, splits, joins, marks, and large deletes.../react-prosemirror/src/components/__tests__/ProseMirror.domchange.test.tsx: tests DOM text-node preservation and typing/mark redraw behavior.../react-prosemirror/src/components/__tests__/ProseMirror.draw-decoration.test.tsx: tests decoration/widget redraw identity..tmp/slate-v2/packages/slate-react/src/hooks/use-generic-selector.tsx:48-137: Slate already uses selector cells plus useSyncExternalStore; do not downgrade to context-wide state reads..tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsx:318-363: runtime tracks mounted view editors per root and root selection cache..tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsx:574-625: useSlateViewState already filters updates by root..tmp/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx:213-387: selector subscriptions already support runtime-id scoped fanout and deferred microtask flush..tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:197-257: runtime root engine already owns composition, selection import/export, and repair bridges.docs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.md: ProseMirror validates transaction discipline, centralized DOM selection import/export, and decorations as view data.docs/research/decisions/slate-v2-react-19-2-perf-architecture-vs-field.md: Slate v2 is React-native and stronger than legacy Slate, but still needs explicit invalidation and redraw proof before claiming field-best perf.docs/analysis/editor-architecture-candidates.md: ProseMirror remains the tier-1 architecture comparison target; Pretext/Premirror remains the future layout lane.| System | Source | Mechanism | Avoids | Steal | Reject | Slate target | Verdict |
|---|---|---|---|---|---|---|---|
| React-ProseMirror | README and useEditorEffect | post-commit editor effect boundary | stale view reads during render | useSlateViewEffect / internal view-effect queue | raw EditorView exposure | effects after Slate view commit, DOM repair, and selection sync | agree |
| React-ProseMirror | useEditorEventCallback | stable callback with latest mounted view | toolbar/event callbacks using stale editor/view | useSlateCommandCallback or equivalent | PM-specific EditorView argument | package-owned control callback with root/focus policy | agree |
| React-ProseMirror | LayoutGroup | deferred grouped layout effects | child layout refs missing when ancestor effect runs | internal grouped Slate view effects | public over-composable effect framework | private boundary first; expose only if app authors need it | partial |
| React-ProseMirror | ReactEditorView / viewdesc | React-managed PM doc view | PM/React DOM ownership conflict | test expectations and pure render/commit split | PM view tree, private superclass hacks, react-reconciler | keep Slate runtime/root engine | diverge |
| React-ProseMirror | reactKeys | transaction-mapped node keys | unstable React keys after document edits | operation-mapped runtime-id stability tests | position-key plugin | prove Slate runtime ids survive edits/composition | partial |
| React-ProseMirror | decorations | equivalent decoration source memoization | rerenders from equal decoration sets | identity-preserving projection source grouping | PM DecorationSet model | stable projection/decorator source identity | partial |
| ProseMirror | research source | transaction + DOM selection authority | ad hoc DOM selection reads | keep Slate transaction/selection owner | integer positions and schema-first identity | operations/commits/root views | agree |
Target shape:
useSlateViewEffect((view) => {
const rect = view.domRangeFromSlateRange(range)?.getBoundingClientRect()
}, { root })
Rules:
Why: this directly answers the react-prosemirror render/view mismatch without exposing ProseMirror-like internals.
Target shape:
const onMouseDown = useSlateCommandCallback((editor, event) => {
editor.update(() => editor.toggleMark('bold'))
}, { focus: 'preserve-active-root' })
Rules:
requestAnimationFrame, restoreFocus, or stale closure helpers.Why: this is the clean answer to toolbar/copy/focus races and the recent multi-root example DX complaints.
Target shape:
React layout effects -> Slate view commit/repair -> Slate view effects
Rules:
SlateViewEffectQueue lives under SlateRuntime;Why: this preserves React purity while letting overlays measure the final DOM.
Target:
Why: this is the perf lesson from DecorationGroup without copying PM decoration objects.
Add proof rows for:
Why: this is the most valuable thing to copy. Perf architecture is only real when DOM identity is protected by tests.
Target:
Why: react-prosemirror explicitly avoids even equivalent selection writes during composition; Slate should keep that rule visible and tested.
react-reconciler dependency. Too version-coupled and unnecessary with Slate selectors/runtime.EditorView subclassing. Slate owns its runtime; copying private view hacks would be architectural debt.ViewDesc as Slate’s primary DOM map. Slate should keep root views plus runtime ids, not integer positions.Candidate names:
useSlateViewEffect(effect, options?)useSlateCommandCallback(callback, options?)Better naming rule:
ViewEffect for DOM/layout work after the root view is committed.CommandCallback for event handlers that will mutate/read Slate.useEditorEffect; it is too broad and too easy to misuse.Default root behavior:
Editable, use that root;Editable, use active root;root override for explicit external controls.Focus policy:
focus: 'preserve' | 'restore-root' | 'none';SlateRuntime;useSlateActiveRoot / useSlateRootEditor;Before: examples can need local helpers like focus restoration, timeout/RAF timing, or stale editor closures.
After:
const toggleBold = useSlateCommandCallback((editor) => {
editor.update(() => editor.toggleMark('bold'))
})
return <button onMouseDown={toggleBold}>Bold</button>
This is the user-facing win: examples show Slate API, not browser timing duct tape.
useSlateCommandCallback and useSlateViewEffect without wrapping every root manually.editor.update, not mutate view-local state.Live generated ledger read: docs/slate-issues/gitcrawl-live-open-ledger.md.
Candidate related rows from current live ledger:
| Issue | Cluster | Claim | Why | Proof route | V2 sync ledger | PR line |
|---|---|---|---|---|---|---|
| #5961 | singleton | Related | React warning during render/event timing; view-effect boundary may reduce class | hook unit + browser command callback row | unchanged this pass | related matrix only |
| #5813 | singleton | Related | decorator/render debugging instability; projection identity tests are relevant | decoration redraw tests | unchanged this pass | related matrix only |
| #5436 | singleton | Related | sticky toolbar needs post-commit measurement/control callback | toolbar example/browser row | unchanged this pass | related matrix only |
| #4483 | singleton | Related | dynamic decoration perf maps to projection identity stability | decoration identity perf tests | unchanged this pass | related matrix only |
| #5131 | singleton | Related | useSlate rerender pressure maps to scoped subscriptions | selector fanout tests | unchanged this pass | related matrix only |
| #5433 / #5398 | singleton | Related | composition re-render/caret movement maps to composition freeze policy | IME browser rows | unchanged this pass | related matrix only |
No Fixes #.... claim in this plan. Next pass must decide whether to update docs/slate-issues/gitcrawl-v2-sync-ledger.md, docs/slate-v2/ledgers/fork-issue-dossier.md, and docs/slate-v2/ledgers/issue-coverage-matrix.md for these related rows. PR description unchanged in this pass.
| Surface | Proof |
|---|---|
| post-commit view effect | unit test: effect sees DOM after selection/export repair |
| command callback freshness | React test: stable handler uses latest root view after root focus changes |
| toolbar focus policy | Playwright: toolbar click preserves intended editor/root selection |
| DOM identity | Vitest/JSDOM or browser: unchanged sibling DOM nodes remain identical across text insert, split, join, mark toggle |
| decoration identity | unit/browser: irrelevant decoration source update does not redraw unaffected blocks/widgets |
| composition freeze | browser/IME rows: composition text not overwritten; selection repair paused unless mandatory |
| multi-root | Playwright: command callback uses active root, explicit root override works |
| Lens | Status | Finding | Plan delta |
|---|---|---|---|
| vercel-react-best-practices | applied | render phase must remain pure; effects/callbacks belong after commit | add view-effect queue and stable callback target |
| performance-oracle | applied | proof needs DOM identity preservation and source-scoped projection identity | add redraw identity tests |
| performance | applied | large-doc perf requires repeated-unit DOM churn budgets | add sibling/widget/decorator non-redraw rows |
| tdd | applied | behavior needs tests before implementation | make proof matrix red-green owner |
| shadcn | skipped | no UI component implementation in this plan | none |
| react-useeffect | applied | effect is external DOM/editor synchronization, not render calculation | use dedicated post-commit hook, not ad hoc app effects |
| Change | Likely objection | Antithesis | Response | Verdict |
|---|---|---|---|---|
add useSlateViewEffect | another hook in an already large React API | app authors can use useLayoutEffect manually | manual layout effects read stale root/view state; package-owned timing prevents toolbar/overlay footguns | keep |
add useSlateCommandCallback | command helper could become opinionated Plate API | raw Slate can keep plain handlers | raw Slate still needs fresh selection/root/focus policy; helper is unopinionated command plumbing | keep |
| decoration identity grouping | internal complexity | current selectors may be enough | react-prosemirror proves equivalent decoration identity matters; keep internal and test-driven | keep |
| DOM identity perf tests | brittle implementation tests | public behavior tests should be enough | redraw identity is a performance contract; tests should assert DOM identity only for stable unaffected nodes | keep |
useSlateViewEffect runs too early and measures stale DOM.
| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.93 | current Slate selectors and runtime root filtering; react-prosemirror render/commit evidence |
| Slate-close unopinionated DX | 0.88 | proposed hooks are root/editor primitives, not Plate toolbar APIs |
| Plate and slate-yjs migration backbone | 0.85 | Plate overlay/control path clear; slate-yjs unaffected but not yet ledger-reviewed |
| Regression-proof testing strategy | 0.84 | proof matrix named; tests not implemented and issue pass pending |
| Research evidence completeness | 0.89 | local react-prosemirror source, current Slate source, compiled ProseMirror/React research |
| shadcn-style composability | 0.88 | minimal hooks, explicit options, no component opinion |
Weighted score: 0.879.
Threshold status: ready for user review as a scoped steal/reject plan. The
implementation-readiness score stays below release threshold because no Ralph
execution or .tmp/slate-v2 proof has run.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| current-state read and initial score | complete | react-prosemirror source, current Slate runtime source, research docs, live issue ledger skim | accepted steal/reject list and score | none | slate-ralplan |
| related issue discovery | skipped | live ledger candidate rows | no durable sync for this scoped planning-only answer | no implementation or fixed-issue claim | none |
| issue-ledger pass | skipped | live ledger skim | no issue matrix update | no fixed claims | none |
| intent/boundary and decision brief | complete | sections above | decision brief recorded | none | slate-ralplan |
| research/ecosystem synthesis | complete for current pass | synthesis table | accept/reject mechanisms | react-prosemirror is not in compiled research yet | slate-ralplan |
| performance/DX/regression pressure | complete | proof matrix | harden future Ralph proof rows | no tests yet because this is planning-only | ralph |
| maintainer objection ledger | complete for current pass | table above | keep narrow hooks/tests | needs expansion if public API changes | slate-ralplan |
| high-risk deliberate mode | complete for current pass | pre-mortem above | require proof rows | needs implementation proof later | slate-ralplan |
| issue sync accounting | skipped | no implementation/fixed issue claim | PR and issue ledgers unchanged | none | none |
| closure score and final gates | complete | scoped closure note | ready for user review | implementation remains future Ralph work | user |
useSlateViewEffect;useSlateCommandCallback;useEditorEffect.Planning-only:
cwd: /Users/zbeyens/git/plate-2
node tooling/scripts/completion-check.mjs
Implementation later:
cwd: /Users/zbeyens/git/slate-v2
bun --filter slate-react test:vitest
bun --filter slate-react typecheck
PLAYWRIGHT_BASE_URL=http://localhost:3100 PLAYWRIGHT_RETRIES=0 bun run playwright <focused Slate React examples/tests> --project=chromium --workers=1
bun lint:fix
react-reconciler, PM EditorView, PM ViewDesc, position-key plugin..tmp/slate-v2 verification is deferred to Ralph implementation because this
skill did not edit .tmp/slate-v2;.tmp/slate-v2/packages/slate-reacttdd-passuseSlateViewEffect and
useSlateCommandCallback, then add DOM identity/projection/composition proof
rows as follow-up slices.active goal state.slate-react Vitest pattern, bun --filter slate-react typecheck, and bun lint:fix before final closure.useSlateViewEffect;useSlateCommandCallback;Editable root;<SlateRuntime> and
<Slate>;.tmp/slate-v2/packages/slate-react/src/context.tsx.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/packages/slate-react/src/components/slate.tsx.tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsx.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/test/rendered-dom-shape-contract.tsx.tmp/slate-v2/packages/slate-react/test/use-slate-view-command-hooks.test.tsxbun --filter slate-react test:vitest -- use-slate-view-command-hooksbun --filter slate-react test:vitest -- rendered-dom-shape-contractbun --filter slate-react test:vitest -- slate-runtime-provider-contractbun --filter slate-react test:vitest -- provider-hooks-contractbun --filter slate-react test:vitest -- selection-runtime-contractbun --filter slate-react test:vitest -- selection-side-effect-policy-contractbun --filter slate-react typecheckbun lint:fix{ deps: [] } view effects missed later
editor commits;react-reconciler..tmp/slate-v2 focused gates deferred to Ralph implementation;done for this scoped planning request.