ReviewOS

stacks/stx

onMount runs before refs inside a component slot are attached (ref.current is null)

#2142
Closed chris opened this 2 days ago ·
2 days ago

What happens

A page puts its game canvas inside a component's slot:

<SiteShell>
  <canvas ref="demoMap" ...></canvas>
</SiteShell>

and mounts it from onMount:

const demoCanvas = useRef<HTMLCanvasElement>('demoMap')
onMount(() => { if (!demoCanvas.current) return; start(demoCanvas.current) })

demoCanvas.current is still null when onMount runs, so the guard returns and nothing starts, with no warning. Deferring one tick makes it work: onMount(() => { void nextTick(() => start()) }). The same pattern with a canvas that is not inside a component slot (a plain page) mounts fine.

Expected

onMount runs after the page's refs, including those in slotted component content, are attached, or the docs say to use nextTick for them. A silent null is the worst of both.

Observed in stx 0.2.756 building the home page of https://github.com/stacksjs/the-settlers-iii-remake (resources/views/index.stx, the startDemo guard).

5 hours ago

Verified the actual affected application against published packages. The STX lifecycle fix is already shipped; no additional runtime change is needed.

Source fix: ace650e51f04d26fc565c4183b9a5d92380e0d4e, originally released in STX 0.2.774. Latest main checked: 81294bc76cc0c86a6ec481e37a292c3d5fa10e49 (0.2.784).

Reproduction and served-runtime identity

Used a temporary clone of stacksjs/the-settlers-iii-remake at c0cd9ddcffa8f15d3e2cde44d0b18b979497289e, a frozen public-registry install, and its real ./buddy serve entrypoint on an owned loopback port. Removed the home page's nextTick workaround.

  • Original app pins: Stacks/actions/Buddy 0.75.390, top-level STX 0.2.774.
  • Both actions and Buddy actually resolve their own nested STX / bun-plugin-stx 0.2.752.
  • The served /_stx/runtime.9932c59a54ef8387.js response is byte-identical to generateSignalsRuntime() from Buddy's nested 0.2.752 package. SHA-256: 063902e45d6b7729a0e49de1c9754c91bb242a0cdb2d5d824289a47c56bc05be.
  • With onMount(() => startDemo()), the canvas exists but the old-runtime demo remains at “Preparing the original graphics and your settlement...”, with none yet stock and ten disabled demo controls.

Published-package verification without the workaround

In that temporary checkout only, installed the matching public Stacks/actions/Buddy/testing 0.75.422 cohort and STX/components/bun-plugin-stx 0.2.784. The frozen reinstall passes. Craft remains pinned to 0.0.396, unchanged.

  • The actual served /_stx/runtime.903c5534bc53856e.js response is byte-identical to the installed 0.2.784 generateSignalsRuntime() output. SHA-256: 5532b87eca8a1a1fc7568283b0ab9d00906f14b455bde10cf5dfdf1cb2670640.
  • A synchronous diagnostic snapshot taken inside onMount, before startDemo(), records CANVAS. No nextTick/timer/deferred mount was added.
  • Imported the original assets using the app's maintained checksum-verifying importer. Real Chrome, through installed @stacksjs/testing/browser, reaches the running guest demo: log 8, plank 30, stone 26, bread 8, meat 8, fish 8; all demo controls enabled.
  • Repeated in a fresh browser context after removing the diagnostic instrumentation, with the page's only source diff being onMount(() => startDemo()): same successful result. Page settled; 41 HTTP responses, zero failed HTTP responses, zero browser errors, zero requests left in flight. Nothing was intercepted or mocked in the successful browser run.
  • Updated consumer bun.lock SHA-256: 4c48188d87a7d52d1c79cfd8239abf7cedc6e2fa077be88683e782fdf5a05778.
  • Maintained STX browser/lifecycle/manifest/runtime-parse cohort: 80 passed, 0 failed, 167 assertions across 6 files, including both mount-after-slotted-refs-browser.test.ts shell variants.

Rollout boundary: this verifies the fix in the actual game source with installed released dependencies; it does not claim the game's main branch or deployed site was upgraded. Its current 0.75.390 pins still serve 0.2.752. The game owner needs the framework cohort bump (0.75.422 was verified here), then can remove the workaround. Changing only the top-level STX dependency does not change Buddy's served runtime.

Closing the STX defect with this consumer evidence. No new STX release, Craft edits, or game-main changes were made. The unrelated user-owned Map test modification is preserved.

2 days ago

Not fixed yet. What is known: the page runtime queues onMount callbacks per component scope in signals-lifecycle-runtime (scope.__mountCallbacks) and flushes them when the scope mounts; a ref declared in the page's own markup but projected into a component slot is attached by hydration after the scope's first flush in my case, so the ref's .current is still null inside the callback. Deferring one tick (void nextTick(() => start())) is the working pattern and the one the Settlers III remake uses (resources/views/index.stx). A fix needs a real-browser regression for a ref inside a slotted component, which this issue still needs.

2 days ago

Update: still reproducible in the Settlers III remake on stx 0.2.762 (removing the nextTick leaves the demo unstarted), but I could not reduce it to a minimal fixture. A page with a canvas ref inside a plain slot component mounted correctly in a standalone fixture in some runs, and my runs were inconsistent. A candidate cause from reading the runtime: the page's global mount callbacks are flushed right after the page root is processed (signals-dom-ready-runtime.ts, before the component-scope loop), while refs inside a component scope are attached when that scope hydrates. Deferring that flush when the root contains component scopes is the obvious fix, but my happy-dom regression did not model the hydration faithfully (the ref stayed null with and without the change) and the real-browser fixture runs were inconclusive, so I pushed nothing. Leaving this open with that finding.

2 days ago

Reopening: the fix in v0.2.774 (defer the page's mount until its component scopes hydrate) covers the DOM-ready hydration path and has a real-browser regression, but the Settlers III remake still sees a null ref in onMount on 0.2.774 (removing its nextTick leaves the demo unstarted). Its page hydrates through the server manifest path (signals-manifest-hydration-runtime), which flushes mount callbacks on its own. Working on that path next.

2 days ago

Why the remake still saw the null ref on 0.2.774: its dev and production servers take the client runtime from the stx that @stacksjs/buddy and @stacksjs/actions pin (0.2.752 in stacks 0.75.390, nested under buddy/node_modules), not from the app's own top-level stx. Setting the app's stx to 0.2.774 changes nothing for the served runtime. The v0.2.774 fix is in the published dist (signals-dom-ready-runtime.js) and passes its real-browser regression, so this closes for the remake once a stacks release pins stx 0.2.774 or later and the app adopts it. Leaving the issue open until that is verified in the remake with its nextTick workaround removed.

Sign in to comment on this issue.