Skip to content

components: <Notification> can't react to client signals (server-rendered @if(show)) #1692

@glennmichael123

Description

@glennmichael123

What

@stacksjs/components's <Notification> is the package's only toast/alert primitive, but it's authored as a <script server> SFC. Its show prop is captured at SSG/SSR time via @if(show), so consumers can't drive it from client-side signals — the typical "fetch returns → toast appears" flow.

Repro

In any consuming app (e.g. a coming-soon page wired to a /api/email/subscribe POST), bind reactive client signals to the component:

<script client>
  const toastVisible = state(false)
  const toastTitle   = state('')
  const toastMessage = state('')
  const toastType    = state('success')

  async function submitEmail() {
    const res = await fetch('/api/email/subscribe', { method: 'POST', /* ... */ })
    if (res.ok) {
      toastTitle.set('Thanks!')
      toastMessage.set('Stay tuned for more updates.')
      toastType.set('success')
      toastVisible.set(true)
    }
  }
</script>

<Notification
  :show="toastVisible()"
  :title="toastTitle()"
  :message="toastMessage()"
  :type="toastType()"
  position="top-right"
/>

What happens

  • The Notification.stx <script server> block evaluates once at render time, so $props.show, $props.title, etc. all resolve to their initial (empty) values.
  • The rendered HTML inlines the package's markup with bg-blue-50 (the default info type) and empty title/message paragraphs.
  • toastVisible.set(true) later in submitEmail() doesn't change anything in the DOM — the :show binding is left as a stray attribute on the rendered <div>, never re-evaluated.
  • Inspecting the SSG output:
    <div :title="toastTitle()" :message="toastMessage()" :type="toastType()"
         class="fixed z-50 m-4 max-w-md ... bg-blue-50 text-blue-900 ..."
         role="alert">
      <div class="flex items-start">
        <div class="flex-1"></div>  <!-- empty: title/message captured as '' at SSG time -->
        ...
      </div>
    </div>

Expected

A toast/notification primitive in a UI component library is almost always driven by client state (button click → show → auto-dismiss). The current <Notification> only fits the static-flash-message case ("on this page render, was the form submitted? Yes → render the green box once"). For the dynamic case, consumers have to either:

  1. Copy Notification.stx locally and rewrite <script server><script client>.
  2. Hand-roll a bespoke toast and hope they replicate the package's design tokens correctly.
  3. Use the package's CSS classes (bg-green-50, text-green-900, etc.) on a custom client-rendered element — works only if the Notification.stx source is in the consumer's class-extraction path so Crosswind generates the utilities.

None of those are obvious from the README, and (3) requires understanding the Crosswind extraction model.

Proposal

Convert the <Notification> SFC from <script server> to <script client> so its props can be reactively bound. Concretely:

  • Move prop derivation (show, title, message, type, position, duration, onClose, className) to client-side state()/derived() (or accept signals directly and read with ()).
  • Replace @if(show) with :if="show()" (or always render and toggle visibility via x-class/data-* attributes for nicer transitions).
  • Replace {{ title }} / {{ message }} with :text="title()" / :text="message()".
  • Leave the existing setTimeout auto-hide block, but wire it through the client signal.

If a static/SSR usage is still wanted, expose a sibling <NotificationStatic> (or accept a static prop that opts out of the signal layer) so apps that just want a one-shot flash message keep working.

Happy to PR this if there's a preferred direction — the current behaviour was a real footgun while wiring up a notify-me toast in a downstream app.

Repo / version

  • @stacksjs/components@0.2.35
  • @stacksjs/stx@0.2.52
  • bun 1.3.13

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions