docs(divergences): reframe activeId as the general composable-ref divergence (#132)

The `useFocusManager().activeId` entry conflated two things and buried the
load-bearing one. Split and reframe:

- The real divergence is framework-semantic, not API-specific: a React hook
  re-runs each render so it can return a plain snapshot, whereas a Vue
  composable's setup() runs once and must wrap reactive state in a `shallowRef`.
  Moved to the Vue != React section as a general rule; `activeId` is now just
  one example of it.
- Folded the empty-value convention (`null` vs Ink's `undefined`) into that
  entry as a Vue ecosystem idiom rather than a separate headline.

Doc-only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-03 17:45:57 +08:00
committed by GitHub
parent b7690050fa
commit 11e9a2e684
+15 -10
View File
@@ -43,16 +43,6 @@ deliberate. Divergences fall into a few kinds:
- **Why:** vue-tui's renderer keeps a native host-node tree rather than a DOM emulation,
so the exported node type names that tree, not a DOM node.
### `useFocusManager().activeId` empty value is `null`, not `undefined`
- **Ink:** `useFocusManager().activeId` is a `string` and `undefined` when nothing is focused.
- **vue-tui:** `activeId` is a **`ShallowRef<string | null>`** — reactive, and `null` (not
`undefined`) when nothing is focused.
- **Why:** vue surfaces focus state as a reactive ref so a template re-renders when focus
moves, and `null` is vue-tui's house convention for an empty ref (a deliberate "no value",
distinct from an unset `undefined`). Field meaning is unchanged. Test:
`focus-manager.test.tsx` ("activeId is null when nothing is focused").
### Second `mount()` on a live stdout is an inert no-op
- **Ink:** `render()` keeps one instance per stdout (`WeakMap<WriteStream, Ink>`); a second
@@ -290,6 +280,21 @@ unsubscribe(){}}`) — a `useAnimation` rendered outside an Ink tree never ticks
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.
### Reactive composable state is a `shallowRef`, not a plain snapshot
- **Ink/React:** a hook re-runs on every render of its component, so it can return a plain value
and the caller always reads the latest one. (`useFocusManager().activeId`, for instance, is a
bare `string | undefined`, re-read fresh each render.)
- **vue-tui:** a composable's `setup()` runs **once**, so reactive state can't be a plain
snapshot — it would freeze at setup time. vue-tui returns a **`shallowRef`** whose `.value`
updates and re-renders the template; read these as `.value`. Every stateful composable follows
this — `useTerminalSize()`, `useFocusManager().activeId`, … — and an empty one holds `null`
(Vue's convention for an empty ref: a template ref is `ref<T | null>(null)`), where Ink's plain
value is `undefined`.
- **Why:** the two frameworks track a changing value differently — React reads the newest value
by re-running the hook, Vue wraps it in a ref the template subscribes to. This is the general
rule, not a per-API choice; `useFocusManager().activeId` is just one instance.
### Out-of-type style values are forwarded, not defensively coerced
- **Ink:** several flex/align setters coerce a runtime junk value to a default — `flexShrink`