fix(runtime): correct misleading measureElement() timing guidance (#139)

A bare `measureElement()` called inside `watchPostEffect` reads layout BEFORE
the commit scheduler's post-flush `calculateLayout` runs, so it returns an
uncomputed value (NaN for computed dimensions), not the current size. The JSDoc
previously recommended that exact broken call site.

Align the guidance to vue-tui's real post-flush timing: defer the read with
`nextTick(() => measureElement(ref.value))` — the pattern `useBoxMetrics` itself
uses — or read from an input/timer callback that fires after a flush; prefer
`useBoxMetrics` for reactive metrics. Also correct the stale claim that a
pre-layout read returns `{0,0}` (it returns NaN for an attached-but-uncomputed
node; `{0,0}` is only the detached case).

Adds a characterization test pinning bare-watchPostEffect = NaN vs
nextTick = real width (80), guarding against regressing to the old advice.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-05 10:43:52 +08:00
committed by GitHub
parent a31e8fa335
commit c05cb5df36
2 changed files with 62 additions and 5 deletions
@@ -71,12 +71,30 @@ function findRootNode(node: TuiNode | null): TuiRoot | null {
/**
* Imperative function that reads yoga computed dimensions from a TUI node.
*
* Returns `{ width: 0, height: 0 }` before layout (when yoga node doesn't
* exist or hasn't been calculated).
* Returns `{ width: 0, height: 0 }` when the ref is not attached to an element.
*
* Note: `measureElement()` returns `{width: 0, height: 0}` when called during
* render (before layout is calculated). Call it from post-render code, such as
* `watchPostEffect`, `onMounted`, input handlers, or timer callbacks.
* Timing matters: layout is computed inside the commit scheduler's post-flush
* callback, which can run *after* your own `watchPostEffect`/render-time code in
* the same flush. A bare `measureElement()` called there reads layout that has
* not been recalculated yet, so it does not return the current size. Read it
* only *after* the layout commit:
* - wrap the read in `nextTick(() => measureElement(ref.value))` (the pattern
* {@link useBoxMetrics} itself uses), or
* - call it from an input handler or timer callback that fires after a flush.
*
* For reactive metrics that stay in sync across renders and resizes, prefer
* {@link useBoxMetrics}, which handles this timing for you.
*
* @example
* ```tsx
* const boxRef = ref(null);
* watchPostEffect(() => {
* // Defer the read so it runs after layout is committed.
* void nextTick(() => {
* const { width } = measureElement(boxRef.value);
* });
* });
* ```
*/
export function measureElement(node: unknown): { width: number; height: number } {
const tuiNode = resolveYogaNode(node);