Scaling with Signals
Coming Soon
Signals are part of the Silvertea app architecture (era2b) and have not shipped yet. This page documents the planned API. For current state management, use useState, useReducer, or Zustand directly.
As your app grows, selectors show their cost. Zustand runs every selector on every store update — 100 <Row> components each with useApp(s => s.rows.get(id)) means 100 selector calls when the cursor moves, even though only 2 rows changed.
Signals (TC39 proposal, stage 1) flip this. Components call signal accessors and automatically subscribe to exactly what they touched — no diffing, no linear scan. Same model as SolidJS and Angular. We use alien-signals — the fastest signals implementation (~1KB, push-pull, proven by Vue 3.6 adoption).
With signals, the factory returns a plain object — signals are the reactive state, so you don't need Zustand's set(). Read with sig(), write with sig(newValue):
import { signal, computed, batch } from "@silvery/signals"
const app = createApp(
() => {
const cursor = signal(0)
const items = signal([
{ id: "1", text: "Buy milk", done: false },
{ id: "2", text: "Write docs", done: true },
{ id: "3", text: "Fix bug", done: false },
])
const doneCount = computed(() => items().filter((i) => i.done).length)
return {
cursor,
items,
doneCount,
moveCursor(delta: number) {
cursor(clamp(cursor() + delta, 0, items().length - 1))
},
toggleDone() {
const i = cursor()
items(items().map((item, j) => (j === i ? { ...item, done: !item.done } : item)))
},
}
},
{
key(input, key, { store }) {
if (input === "j") store.moveCursor(1)
if (input === "k") store.moveCursor(-1)
if (input === "x") store.toggleDone()
if (input === "q") return "exit"
},
},
)signal() creates reactive state — call it to read, call with a value to write. computed() derives from signals — doneCount recomputes only when items changes, not on cursor moves. batch() groups multiple signal writes into a single notification:
batch(() => {
cursor(0)
items(newItems)
filter("")
})
// → one notification, one re-renderSignals are orthogonal to the levels — you can use them at Level 2 or Level 5. They're a performance optimization, not a conceptual shift. If your app doesn't have performance issues with selectors, skip them.
Silvery:
@silvery/signalsprovides the reactive core (alien-signals) pluscreateStore()for deep objects,createResource()for async, and/reactbindings (useSignal). React integration viauseSyncExternalStore.
Scaling to Thousands of Items
Your todo list has 5,000 items and the cursor stutters. Two techniques help at any level:
Per-entity signals — Map<string, Signal<T>> gives each item its own signal. Edit one item → 1 re-render:
const cursor = signal<string>("item-0")
const items = new Map<string, Signal<ItemData>>()
return {
cursor,
items,
currentItem: computed(() => items.get(cursor())?.()),
updateItem(id: string, data: ItemData) {
const s = items.get(id)
if (s) s(data) // only this item's subscribers re-render
},
removeItem(id: string) {
items.delete(id) // clean up — stale signals leak memory
},
}VirtualList — only mount the ~50 visible rows. Combined with per-entity signals: edit one item → 1 re-render. Move cursor → 2 re-renders. O(visible), not O(total).
See Also
- Building an App — the guide that introduces shared stores at Level 2
- State Management — createApp, createSlice API reference
- Hooks Reference — useApp, useBoxRect