A Signals Store
In Marko 6, data flows explicitly — props down the tree, events back up — rather than through a context API. This covers a component and its
children. Either way, a <context> api is coming soon! But when two components that aren’t parent and child need the
same state — a cart badge in the header, an “add to cart” button three
sections away — the cleanest answer is to lift that state out of the tree
entirely, into a module every component can import.
This project shares a counter between two independent components:
counter-controls (the buttons) and counter-display (the readout).
Neither sits inside the other — they’re siblings on the page — yet they
stay in sync.
The store
src/shared/store.js holds the shared state as a signal from
@preact/signals-core:
import { signal } from "@preact/signals-core";
export const count = signal(0);export function increment() { count.value++; }A signal is a reactive box: count.value reads or writes what’s inside,
and anything watching is notified the moment it changes. The module also
exports the actions that mutate it, so no component reaches for
count.value on its own — counter-controls just calls them:
import { increment } from "../shared/store.js"<button onClick() { increment() }>+</button>That’s the write side, and it’s already wired. Click + and the signal’s value really does change. But the readout doesn’t move — nothing is listening yet.
Listening from a component (your job)
Marko’s reactivity and the signal are two separate systems, so you bridge
them: subscribe to the signal, and push each new value into a Marko
<let>. Open src/tags/counter-display.marko and fill in the <script>:
<script> $signal.onabort = count.subscribe((v) => { value = v });</script>A signal’s subscribe(fn) calls fn right away with the current value,
then again on every change, and hands back an unsubscribe function.
Assigning that to $signal.onabort — the same cleanup handle from the
stopwatch and the SSE feed — tears the subscription down when the
component unmounts. Each call sets the value let, and that assignment
is what re-renders the readout.
Now work the buttons. Both components move together: the controls write to
the store, the display reads from it, and neither knows the other exists.
Add a third component that imports count and it would track along too —
the store doesn’t care who’s listening.
That’s a central store: shared state living in a module, changed through exported actions, and bridged into any component that needs to watch it — no context, no prop-drilling, no parent in common.
- Installing dependencies
- Starting dev server