ReviewOS

stacks/stx

A scoped slot inside a :for loop hangs the page render

#2101
Closed glennmichael123 opened this 8 days ago · {{ comments.length }} {{ comments.length === 1 ? 'comment' : 'comments' }}
8 days ago

Adding a scoped slot to a component whose <slot> sits inside a :for loop stops the page rendering entirely. Not a wrong value, not a missing element - the response never completes.

Found on @stacksjs/stx@0.2.607, npm's latest.

Reproduction

A working page: a Stacks dashboard table with 50 rows, already using a #toolbar slot on the same component, rendering fine.

Add one scoped slot whose host <slot> is inside the row loop:

<Table :columns="COLUMNS" :data="visibleRows()">
  <template #actions="{ row }">
    <Button variant="ghost" size="sm" @click="console.log(row)">Edit</Button>
  </template>

  <template #toolbar> … unchanged … </template>
</Table>

The host renders that slot as:

<template :for="row in visibleRows()">
  <tr>
    …
    <td :if="$slots.actions" class="px-4 w-12">
      <slot name="actions" :row="row" />
    </td>
  </tr>
</template>

Result:

curl -s -o /dev/null -w "%{http_code} %{size_download}" http://localhost:3002/dependencies
000 0          # hangs, nothing returned

Remove only that <template #actions> block, change nothing else, restart:

200 548823

The dev server stays up and logs no error for it. In the browser the document sits at readyState: "loading" with an empty body, so it reads as a slow page rather than a failure.

What is not the cause

  • Not compilation. compileTemplate() on the same file completes and the output looks right: the <th>, the <td :if="$slots.actions">, the slot body with its @click="openEdit(row)" binding, and the resolved <Button> scope are all present.
  • Not the #toolbar slot. That one is on the same component, outside the row loop, and has worked throughout.
  • Not $slots in a compound condition. The guard here is the standalone $slots.actions, which is the form that works for #2098.

The difference between the two slots on that component is that #toolbar expands once, outside any loop, and #actions expands per row inside <template :for>.

Why I am reporting it rather than working around it quietly

The workaround is easy - a declarative prop instead of a slot - but the failure mode is not. A slot that renders nothing is findable. A slot that makes the whole page hang, with no error anywhere and a server that stays healthy, costs much more to track down, and the natural first guess is the data layer rather than the template.

Context: this is the actions column of a 729-line table component in Stacks that 42 dashboard tables are being migrated onto (stacksjs/stacks#2925). It is now using props instead, so nothing is blocked - but the slot is part of that component's public shape and other callers will reach for it.

Possibly already fixed on main: v0.2.609 carries 1f167716d1 fix(slots): expose per-instance presence to reactive expressions, and v0.2.612 carries 9d783de44a test(slots): cover nested table projection and visible interpolation, neither of which is published. I will re-test the moment a release lands and close this if it is gone.

8 days ago

Rechecked against latest main at ca08a3f (v0.2.634 source), not the v0.2.607 installation in the report. I could not reproduce the HTTP hang.

Added and pushed regression coverage in 2d41f7c (test-only; no runtime fix claimed):

  • packages/bun-plugin/test/serve-scoped-slot-loop.test.ts: the real shared Button inside #actions="{ row }", projected into a guarded slot inside a client loop. Both HTTP hosts, development and production, return 200 for the toolbar-only control, the actions page, router navigation, and repeat requests. Each host runs in a subprocess with bounded HTTP requests so a hanging renderer cannot stall the test runner. Renderer imports explicitly target source.
  • packages/stx/test/components/scoped-slot-actions-runtime.test.ts: default and compiled/minified output hydrate 50 real Buttons; clicking dispatches the correct row; replacing/reordering the data retains correct handlers; clearing the data removes all Buttons. This uses the maintained DOM harness, not a device/browser visual check.
  • A temporary HTTP probe substituting the current full Stacks Table source also completed, without replacing Button with a stub. This was a standalone caller, not the original dashboard server or its full configuration.

Validation: 440 passed, 0 failed across 61 component/HTTP test files; lint and formatting checks passed on the changed files.

Leaving this open for an original-page retest on a newer released installation. These checks establish that the reported slot shape works on current source, but do not identify the old hang or prove the full Stacks application no longer encounters it. No release was cut as part of this test-only change; v0.2.634 is the source version tested, not a claim about registry publication.

8 days ago

Retested, and this is fixed. It can be closed.

Correction to the original report

My repro was run against 0.2.484, not the version I said. The dashboard dev server was silently preferring a copy out of the project's pantry/ directory over the declared dependency, so every reproduction on that machine ran on a build 123 versions behind the pin. Fixed on our side in stacksjs/stacks@ad1397f; the detail is in stacksjs/stacks#2941.

So "hangs the page render" described 0.2.484 behaviour and should not have been filed against 0.2.607.

What the versions actually do

Same page, same table, same #actions scoped slot inside the :for, measured in a browser after hydration:

stxpagebuttons renderedrow prop
0.2.484stopped responding--
0.2.607200, 1.2-5.6s0 - slot silently dropped-
0.2.631200, 9.4s cold10, one per rowcorrect per row
0.2.634200, 2.8-9.6s10, one per rowcorrect per row

The slot body:

<template #actions="{ row }">
  <button type="button" :data-name="row.name">Edit</button>
</template>

On 0.2.631 and 0.2.634 the ten buttons carry data-name of Electronics, Home & Garden, Clothing, Toys, ... - the scoped prop is bound per iteration, which is the whole thing that was broken.

On 0.2.607 the server emits the slot body once with the binding unevaluated (data-name="row.name"), and hydration then drops it entirely: zero buttons, while the table's own ten rows render fine. So between 0.2.484 and 0.2.631 the symptom changed from a hang to a silent no-op, and somewhere at or before 0.2.631 it started working.

I did not bisect 608-631 to find the exact fix, since both versions we care about are good.

Thanks - this unblocks #actions for the rest of our table migration (stacksjs/stacks#2925), which had been working around it with a declarative rowActions prop.

8 days ago

Closing based on the original-page confirmation in /stacks/stx/issues/2101#issuecomment-6318570694: scoped actions render correctly with per-row props on STX 0.2.631 and 0.2.634. The reported hang was traced to a stale 0.2.484 copy selected from pantry, not the declared version. Regression coverage landed in 2d41f7c (serve-scoped-slot-loop.test.ts and scoped-slot-actions-runtime.test.ts); its recorded validation was 440 passed / 0 failed. No new runtime fix or release is claimed by this closure.

Sign in to comment on this issue.