docs(parity): record the useCursor re-assertion framework-semantic divergence (#110)
Investigating P15 (a stable-reference cursor dropped on an unrelated commit) found the premise was false: Ink does NOT re-assert the cursor on every commit. Ink's useCursor uses a no-deps useInsertionEffect that re-runs only when the cursor COMPONENT re-renders. React re-renders a whole subtree on an ancestor's commit, so Ink re-asserts when the cursor is in that subtree — but when an unrelated SIBLING owns the changing state, the cursor component does not re-render and Ink drops the cursor too. vue (watch on positionRef) already matches Ink in that sibling case and for the recommended reactive usage; the two differ only in the narrow edge of a set-once cursor plus an ancestor-driven commit (Vue's fine-grained reactivity vs React's render cascade). A global per-commit re-assert would diverge from Ink in the opposite (sibling) direction. So this is an unavoidable Vue ≠ React consequence: document it and keep the reactivity-tied behavior rather than "fix" it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,29 @@ deliberate. Divergences fall into a few kinds:
|
||||
f(current props): no `display` set → the default (visible). Persisting a withdrawn prop,
|
||||
or flipping it to hidden, is the anomaly. Maintainer decision (2026-05-31): KEEP.
|
||||
|
||||
### `useCursor()` re-assertion follows fine-grained reactivity, not React's render cascade
|
||||
|
||||
- **Ink:** `useCursor`'s no-deps `useInsertionEffect` (`use-cursor.ts:27-32`) re-runs on every
|
||||
render **of the cursor component**, re-marking the cursor dirty (`ink.tsx:494-497`); log-update
|
||||
resets `cursorDirty` each commit. React re-renders a whole subtree when an ancestor commits, so
|
||||
if the cursor component is in that subtree it re-renders and the cursor is re-asserted — even
|
||||
when only an ancestor's unrelated state changed. (If an _unrelated sibling_ owns the changing
|
||||
state, the cursor component does **not** re-render, so Ink does **not** re-assert and the cursor
|
||||
is dropped that commit.)
|
||||
- **vue-tui:** `useCursor` propagates via `watch(positionRef, …, {flush:'sync'})` — it re-asserts
|
||||
when the position **reference changes** (or the owning component re-renders and re-sets it). Vue's
|
||||
fine-grained reactivity re-runs only components whose own deps changed, so an _ancestor_-driven
|
||||
commit does **not** re-run a cursor child that didn't depend on the changed value, and a
|
||||
set-once cursor is dropped that commit.
|
||||
- **Why:** the two agree for the **recommended** usage — set the position reactively (in the render
|
||||
body / from a ref the component reads), as Ink's apps and vue-tui's parity tests do — and they
|
||||
agree in the unrelated-sibling case (both drop). They differ only in the narrow edge of a
|
||||
**set-once** cursor plus an **ancestor-driven** commit: React's render cascade re-asserts it,
|
||||
Vue's fine-grained reactivity does not. This is a direct consequence of Vue ≠ React (cascade vs
|
||||
fine-grained re-render) and cannot be papered over: a global per-commit re-assert would make vue
|
||||
diverge from Ink in the _opposite_ (unrelated-sibling) direction, where Ink drops the cursor.
|
||||
Keep the reactivity-tied behavior. Maintainer decision (2026-06-01): KEEP.
|
||||
|
||||
## Not applicable in Vue
|
||||
|
||||
### React concurrent mode
|
||||
|
||||
Reference in New Issue
Block a user