State and Async Work
In Vango, the important question is not just what state you need. It is where that state belongs and which boundary owns the work.
Local signals
Allocate local reactive state with setup.Signal.
1count := setup.Signal(&s, 0)
Common operations:
Get()
Peek()
Set(v)
Update(fn)
numeric helpers like Inc() and Dec()
container helpers like Append(...) , RemoveAt(...) , and UpdateAt(...)
Prefer copy-on-write semantics over in-place mutation.
Shared signals, global signals, and session keys
Use the smallest durable primitive that matches the ownership model.
setup.SharedSignal
Session-scoped, shared, and persisted when a store is configured.
Use it for:
cart IDs
cross-page filters
small per-session preferences
setup.GlobalSignal
App-scoped, shared across sessions, and persisted in the global store.
Use it sparingly for things like announcements or system-wide feature flags.
vango.SessionKey[T]
Typed session-scoped persisted key-value storage. Use it when durable state does not naturally belong to a single component allocation site.
Memos
Use setup.Memo for derived state only.
1total := setup.Memo(&s, func() int {
2 sum := 0
3 for _, item := range cart.Get() {
4 sum += item.Qty
5 }
6 return sum
7})
Memos never persist. They derive from reads inside the compute function.
Resources for async reads
Use setup.Resource or setup.ResourceKeyed for blocking reads.
1profile := setup.Resource(&s, func(ctx context.Context) (*Profile, error) {
2 return loadProfile(ctx)
3})
1user := setup.ResourceKeyed(&s,
2 func() int { return props.Get().UserID },
3 func(ctx context.Context, id int) (*User, error) {
4 return loadUser(ctx, id)
5 },
6)
Rules:
loaders may block
loaders must honor context.Context
loaders must not write reactive state directly
keyed resources should be used whenever props, URL state, or local state determine the selection
Render Resource state explicitly with Match(...).
Actions for async mutations
Use setup.Action for async writes triggered from the session loop.
1save := setup.Action(&s,
2 func(ctx context.Context, in SaveInput) (*SavedProfile, error) {
3 return saveProfile(ctx, in)
4 },
5 vango.DropWhileRunning(),
6)
Call Run(...) from an event handler or other session-loop code.
Choose a concurrency policy deliberately:
CancelLatest() for search-like work
DropWhileRunning() for save or delete actions
Queue(n) for ordered background work
If work must outlive the session, use github.com/vango-go/vango-jobs instead of treating jobs as slow Actions.
Transactions
Use vango.Tx(...) or vango.TxNamed(...) when one user action updates multiple pieces of state that should commit together.
That gives you clearer intent, fewer intermediate rerenders, and atomicity for persisted writes.
URL params
Use setup.URLParam for shareable state that belongs in the URL.
1query := setup.URLParam(&s, "q", "", vango.Replace, vango.URLDebounce(300*time.Millisecond))
2page := setup.URLParam(&s, "page", 1, vango.Replace)
Use Replace for filters and search fields. Use Push when each change should become a real navigation step.
Concurrency rules
Do not start manual goroutines in:
Setup
render
event handlers
OnMount
Effect
OnChange
Do not do blocking work on the session loop.
If you need async reads, use Resources. If you need async writes, use Actions. If you need a route-bound POST contract, use a page Action plus setup.RouteForm.
Choosing the right primitive
| local reactive state | setup.Signal |
|---|---|
| derived state | setup.Memo |
| async read | setup.Resource / setup.ResourceKeyed |
| live async mutation | setup.Action |
| route-bound form post | page Action + setup.RouteForm |
| session durable KV | vango.SessionKey[T] |
| session-shared persisted state | setup.SharedSignal |
| app-global persisted state | setup.GlobalSignal |
| shareable URL state | setup.URLParam |
Next, read Building Views with el for the rendering side of the framework.