@@ -411,7 +411,52 @@ Tests are golden-string comparisons of generated JS text (`lang/tests/compiler_p
411411Add: (1) structural tests asserting ` switch ` /` while ` /` throw ` /` % ` appear in output (or that a diagnostic
412412fires); (2) a headless-DOM harness that executes the generated JS and asserts the hydrated DOM (this is
413413how the todo demo was validated during this analysis); (3) SSR-content tests (` <h3>5 left</h3> ` , not
414- ` <h3> left</h3> ` ); (4) move every ` universal_failures.md ` case into the suite.
414+ ` <h3> left</h3> ` ); (4) move every ` universal_failures.md ` case into the suite.
415+
416+ ### 3.10 Reactive render model: pitfalls that break conditional UI (verified, cdm ` ErrorOverlay ` )
417+
418+ While building ` ErrorOverlay ` (a global uncaught-error catcher) in
419+ ` lang/libs/components/src/ErrorOverlay.ch ` , two framework behaviors repeatedly prevented the
420+ overlay from ever appearing. Both are direct consequences of the runtime's
421+ ** "component bodies run once; only derived/conditional JSX nodes re-render"** model (the
422+ ` $_ucs ` computed-signal patcher in ` defaultUniversalSetup ` ).
423+
424+ ** Pitfall A — ` #css ` style helpers are server-only and crash at hydration.**
425+ ` #css { … } ` helpers such as ` error_overlay_styles(page : &mut HtmlPage) : *char ` take the SSR
426+ ` page ` pointer. On the client the component is invoked as ` factory(props) ` with ** no ` page `
427+ argument** , so calling the helper throws during hydration → the component hits the error
428+ boundary (` $__uni_render_fallback ` ) and renders nothing. Symptom: the component is silently
429+ absent and nothing appears, with no visible error.
430+ - ** Workaround (proven):** do not use ` #css ` helpers inside ` #universal ` components that must
431+ render on the client. Use inline ` style={{ … }} ` objects (string/number values) or plain
432+ ` class="…" ` strings, matching the existing app dialogs (e.g. ` CdmApp ` 's Tools dialog uses
433+ ` class ` + inline ` style ` ). For reusable styling, emit the CSS once in a top-level
434+ ` <style> ` /theme and reference class names instead of per-component ` #css ` helpers.
435+
436+ ** Pitfall B — visibility toggles must be a JSX conditional child, never control-flow
437+ ` if ` /` return ` .**
438+ ` if(!open) { return null } ` is evaluated ** once at mount** and is not a reactive binding, so
439+ flipping ` open ` later does nothing. Storing the conditional in a local `var overlay = open ? …
440+ : null` and then ` return overlay` is also one-time — the local is not wrapped in a computed
441+ signal. Only a conditional used ** directly as a JSX child expression** becomes a ` $_ucs `
442+ computed that re-evaluates when its state dependencies change.
443+ - ** Workaround (proven):** render ` {open && errors.length > 0 ? <div …>…</div> : null} ` inline
444+ as a child (wrap in an inert ` <div style={{display:"contents"}}> ` if a single root is
445+ needed). All state reads (` open ` , ` errors ` , ` selected ` , ` copied ` ) must happen * inside* that
446+ conditional so they subscribe. Never ` return ` a precomputed local.
447+
448+ ** Net result:** once both pitfalls were avoided (inline styles + inlined conditional child),
449+ ` window.__reportError(msg, stack) ` and the `window.addEventListener("error" /
450+ "unhandledrejection")` handlers installed in the component's ` useEffect` correctly flip
451+ ` open ` and the modal renders. This is the pattern every show/hide component (dialogs, toasts,
452+ dropdowns, modals) in this framework must follow.
453+
454+ Suggested roadmap additions:
455+ - (N) In ` #universal ` components, hard-warn (or reject) ` #css ` helper usage in positions that
456+ execute during client hydration — or make ` #css ` a no-op that returns a stable class name on
457+ the client.
458+ - (N) Document the "conditional child, not ` if ` /` return ` " rule in the component authoring guide
459+ and ideally lint for early-` return ` -on-state patterns.
415460
416461---
417462
0 commit comments