docs/plans/2026-05-11-slate-v2-use-element-if-hard-cut-ralplan.md
useElementIf Hard-Cut RalplanYes: hard cut useElementIf from the public slate-react API.
Do not cut the capability. Cut the public name. The optional context read is an
internal implementation detail for hooks like useElementSelected(path?), not a
concept users should learn.
This plan is ready for user review and later ralph execution. No Slate v2
implementation files were edited by this Slate Ralplan pass.
useElement() inside a
rendered element, selector hooks for mounted node reads, or purpose-built
hooks like useElementSelected(path?).slate-react public exports, hook naming, internal optional
element context read, docs/examples/proof rows.useElementSelected
behavior, adding compatibility aliases, or touching Plate product APIs.ralph may remove the public export and refactor the
internal call site, but should not add a replacement public hook unless a
downstream usage audit proves a real user case that the selector hooks cannot
cover..tmp/slate-v2/packages/slate-react/src/index.ts:91
exports useElement and useElementIf from ./hooks/use-element..tmp/slate-v2/packages/slate-react/src/hooks/use-element.ts:10 throws outside
render-element context, while :25 returns useContext(ElementContext) and
may be null..tmp/slate-v2/packages/slate-react/src/hooks/use-element-selected.ts:8 imports
useElementIf; :10-15 uses it only to support either context element or an
explicit path.useElementIf:
.tmp/slate-v2/site/examples/ts/huge-document.tsx:220,
.tmp/slate-v2/site/examples/ts/inlines.tsx:280,
.tmp/slate-v2/site/examples/ts/paste-html.tsx:193, and
.tmp/slate-v2/site/examples/ts/mentions.tsx:254 use
useElementSelected..tmp/slate-v2/packages/slate-react/src/hooks/use-node-selector.tsx:110-116
exposes selector-first useNodeSelector, and :129-144 exposes
useTextSelector..tmp/slate-v2/packages/slate-react/src/hooks/use-editor-selector.tsx:153-187
exposes useEditorState(selector, options)..tmp/slate-v2/packages/slate-react/test/use-element-selected.test.tsx:30-230
proves useElementSelected behavior and selected self-removal safety.docs/slate-v2/ledgers/issue-coverage-matrix.md:73 and :200 already claim
Fixes #6053 for useElementSelected, not for useElementIf.docs/slate-v2/references/pr-description.md:109-111 mentions
useElementSelected, and :697 records useElementSelected(path?) as the
accepted current shape. It does not mention useElementIf.bun --filter slate-react typecheck passed from
/Users/zbeyens/git/slate-v2.Principles:
null must pay for that nullable shape with a real
public use case.Top drivers:
useElementIf sounds like a conditional hook and makes users ask
"if what?"Viable options:
useElementIf.
useOptionalElement.
useElementSelected implementation simple,
preserves selector-hook direction.useElementIf must migrate.Chosen option: option 3.
Rejected alternatives:
useOptionalElement: rejected until a real downstream
usage proves selector hooks or explicit path props are insufficient.useContext(ElementContext) everywhere: rejected if more than one
internal optional reader appears; acceptable for the current single call site.useElementIf.useElement() as the strict render-element-context hook.useElementSelected(path?) as the purpose-built selected
element hook.useNodeSelector, useTextSelector, and useEditorState for
selector-based reads.useContext(ElementContext) in
useElementSelected, or keep a non-exported helper named
useOptionalElementContext.useElementIf as deprecated compatibility surface.For users:
renderElement and the element is required: use useElement().useElementSelected(path?).useNodeSelector with an explicit runtimeId or other available context.useElementIf() just to avoid a throw, pass the
path or element through their component boundary explicitly. Silent null
is the wrong abstraction.This is a hard cut because Slate v2 is still shaping the public surface. If the package is treated as already API-frozen, this becomes a one-release deprecation instead, but that is the weaker call.
| System | Source | Mechanism | Slate target | Verdict |
|---|---|---|---|---|
| React | hook model and Vercel React rules applied in this pass | hooks should expose stable intent and avoid extra subscriptions/nullable branches in hot render surfaces | strict context hook plus selector hooks; no nullable public context escape hatch | agree |
| Tiptap | docs/research/sources/editor-architecture/tiptap-extension-command-react-dx.md:71-90 | product DX gives composable React helpers, but selector hooks carry rerender pressure | keep purpose-built hooks and selector hooks; do not expose accidental context plumbing | partial |
| Slate v2 compiled verdict | docs/research/decisions/slate-v2-architecture-verdict-after-human-stress-sweep.md:101-108 | public selector hooks stay model-truth-only; runtime-specific shortcuts stay internal | cut public useElementIf; keep internal optional read only if needed | agree |
No fixed issue claim changes.
| Issue | Cluster | Claim | Why | Proof route | V2 sync ledger | PR line |
|---|---|---|---|---|---|---|
| #6053 | singleton-react-runtime | Preserved fixed claim | useElementSelected() self-removal proof is the issue-facing hook behavior; useElementIf is only an internal support detail. | .tmp/slate-v2/packages/slate-react/test/use-element-selected.test.tsx; docs/slate-v2/ledgers/issue-coverage-matrix.md:73 and :200 | unchanged: docs/slate-issues/gitcrawl-v2-sync-ledger.md:18 remains fixes-claimed | unchanged: Fixes #6053 stays about useElementSelected, not useElementIf |
ClawSweeper status: skipped. Existing ledgers already cover the only touched
issue-facing surface, useElementSelected / #6053; this plan makes no new
fixed/improved issue claim and does not change issue wording.
docs/slate-v2/references/pr-description.md status: unchanged. The reference
already names useElementSelected(path?) and does not mention useElementIf.
useElementIf adds a nullable public branch without owning a
subscription policy.useElementSelected and public-surface/typecheck gates.useElementIf for nullable context reads.
useElement, explicit props, or
selector hooks.useElementSelected loses optional outside-context behavior.
use-element-selected tests.ElementContext or strict useElement.
Blast radius:
.tmp/slate-v2/packages/slate-react/src/index.ts.tmp/slate-v2/packages/slate-react/src/hooks/use-element.ts.tmp/slate-v2/packages/slate-react/src/hooks/use-element-selected.ts.tmp/slate-v2/packages/slate-react/test/use-element-selected.test.tsxuseElementIf| Change | Likely objection | Steelman antithesis | Tradeoff tension | Rejected alternative | Migration answer | Proof required | Verdict |
|---|---|---|---|---|---|---|---|
Cut public useElementIf | "This was just added upstream; why break users?" | A nullable context hook is convenient for custom wrapper components that may mount inside or outside renderElement. | Hard cut breaks imports; users must choose a stricter shape. | Deprecation alias, because it keeps the weird name in docs and autocomplete. | useElement() inside render-element; useElementSelected(path?) for selected state; selector hooks or explicit props outside context. | rg useElementIf; use-element-selected tests; slate-react typecheck; package export/docs scan. | keep |
useElementIf from
.tmp/slate-v2/packages/slate-react/src/index.ts.useContext(ElementContext) in useElementSelected, oruseOptionalElementContext.useElementIf.#6053 proof by running selected-element tests.From /Users/zbeyens/git/slate-v2:
rg -n "useElementIf" packages/slate-react/src packages/slate-react/test site
cd packages/slate-react && bun test:vitest test/use-element-selected.test.tsx test/provider-hooks-contract.test.tsx
bun --filter slate-react typecheck
bun lint:fix
From /Users/zbeyens/git/plate-2:
node tooling/scripts/completion-check.mjs
| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.93 | public nullable context hook cut; selector hooks remain the narrow subscription path |
| Slate-close unopinionated DX | 0.95 | useElement() remains strict and literal; useElementSelected(path?) owns the selected-state case |
| Plate and slate-yjs migration-backbone shape | 0.90 | no Plate product API added; no collab/data-model surface touched |
| Regression-proof testing strategy | 0.92 | useElementSelected proof already covers the behavior; implementation gates preserve it |
| Research evidence completeness | 0.92 | live source plus compiled selector-hook verdict and Tiptap React DX comparison |
| shadcn-style composability and hook minimalism | 0.96 | removes one public accidental hook; keeps purpose-built composable hooks |
Total: 0.93.
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Focused public hook hard-cut review | complete | live useElementIf export/implementation/call-site scan, selector-hook source scan, #6053 ledger scan, slate-react typecheck | accepted hard cut, no alias, internal optional helper only | none for planning; implementation still belongs to ralph | user review, then ralph |
| Ralph hard-cut execution | complete | removed public export; renamed internal helper to useOptionalElementContext; removed current docs mention; grep returned no current source/doc/dist matches; focused tests, typecheck, build, and lint passed | accepted hard cut executed; #6053 accounting unchanged | none | none |
0.93; no dimension below 0.90.done for planning. Execution still requires
explicit ralph.