Summary
A component prop binding — :prop="expr" on a component tag — never writes its attribute when the component sits in an @included partial. The server emits data-stx-parent-bindings="open" on the component root, but no open attribute ever appears, so useReactiveProp has nothing to observe and the component stays at its initial prop value for the life of the page.
Plain directives on plain elements in the same partial, on the same signal are reactive. It is specifically the component-prop path.
Measurement
resources/components/workout-panel.stx in hq.training — an @included partial, included once from resources/layouts/app.stx — with two bindings added side by side on the same signal:
<p id="wp-probe" :text="wpOpen ? 'PANEL-SIGNAL-TRUE' : 'PANEL-SIGNAL-FALSE'"></p>
<Drawer :open="wpOpen()" position="left" title="Probe" @close="wpStore.close()">…</Drawer>One page load, before and after flipping the signal:
| before | after | |
|---|---|---|
#wp-probe textContent | PANEL-SIGNAL-FALSE | PANEL-SIGNAL-TRUE ✅ |
Drawer root open attribute | null | null ❌ |
| Drawer visible | no | no ❌ |
Setting the attribute by hand makes everything work correctly — panel shows, body scroll locks (overflow: hidden), focus moves into the panel:
document.querySelector('[data-stx-scope*="drawer"]').setAttribute('open', 'true')So the component is fine. The binding is what does not arrive.
The rendered root
data-stx-parent-bindings = "open"
data-stx-parent-events = "close"
data-stx-scope = "stx_drawer_2_9am83y"
data-stx-props = {"data-stx-scope":"stx_scope_workout_panel_63","position":"right",
"size":"lg","panelClass":"…","backdropClass":"…"}Two things stand out:
openis listed indata-stx-parent-bindingsbut appears in neitherdata-stx-propsnor as an attribute. Every statically-passed prop made it; only the bound one did not.- The partial's own scope id leaked in as a prop —
"data-stx-scope":"stx_scope_workout_panel_63". That looks like the prop extraction taking an attribute belonging to the partial's scope wrapper, which suggests the binding machinery has the wrong idea about which scope owns the component.
Where it works
The same <Drawer :open="…"> in a top-level view works, both with a local state() and with a useStore() signal — attribute written, drawer opens. The difference is the partial, not the kind of signal:
| Location | Signal | attribute written |
|---|---|---|
| view | state() | yes |
| view | store signal | yes |
@included partial | state() | no |
@included partial | store signal | no |
What I could not reduce
I could not build a standalone minimal partial, because a newly added .stx partial's <script client> did not execute at all in my checkout — its :text directive was stripped from the element and nothing ever set a value, even after clearing storage/framework/stx/bundle-cache/ and restarting the dev server with the file already on disk. Long-established partials in the same project are fine. I am reporting that only as an observation that got in the way; it may be unrelated, and it is why the evidence above is from the real file rather than a fixture.
Possibly related, printed on every dev boot of this project:
[stx:bundler] dropped every binding for workout-detail.ts (61b374780515); serving the unbundled source and not caching this resultImpact
This blocks adopting any stateful component in a partial. Partials are where an app puts the things that exist once and are driven from everywhere — a slide-over, a confirm dialog, a toast host — which is exactly the population that needs a bound open.
Environment
@stacksjs/components0.2.339, stacks 0.75.34, Bun 1.3.x, macOS- Found while migrating hq.training onto the component library (#201).