BESPA’s state model is intentionally tiny: every state variable is a key in the URL’s query string. The browser holds the current state, the server reads it on each request, and RedrawIfChanged decides what re-renders when a value moves.
Every handler starts the same way:
state := wf.StateOf(r)
name := state.Get("name") // empty string if absent
hasName := state.Has("name")
nameChanged := state.Changed("name") // true during a partial redraw if "name" moved
state.Get returns the empty string for missing keys — this is the same shape as url.Values, which is what StateOf is internally.
Links and form submissions targeting ?key=value merge into the page state and trigger an incremental redraw:
// Set state.foo to "bar" — the page redraws with the new value.
wf.Link("?foo=bar").Add("Set foo"),
// Clear state.foo by setting it to empty.
wf.Link("?foo=").Add("Clear foo"),
// Multiple variables in one click.
wf.Link("?foo=bar&panel=").Add("Set foo, close panel"),
Empty values delete the key. & between params lets a single click move several variables at once — useful for mutually-exclusive UI like “open modal, close panel.”
Names starting with _ are reserved by the framework. The ones you’ll see most:
_changed — comma-separated list of state vars that moved on this request. state.HasChanges checks for it; state.Changed queries it._back — return URL used by RedirectBack and the HrefBack-style button helpers. Conventionally set by the calling page so the destination knows where to send the user._target — names a nested page; see Basics → Targeting frames._submit — set automatically on form submissions to name the form.State is a flat key/value store, but a couple of conventions keep it readable:
table_q for quick-search, table_sort for the current sort column. This lets two tables coexist on one page.modal) whose value is the path of the embedded page.form.Values(r) into the same struct field names.State that should survive a refresh or a shared-link paste goes in the URL. State that’s per-user-but-not-shareable goes in the session. The showcase uses an in-memory session keyed by an HttpOnly cookie:
// website/shared/session.go
type Session struct {
ID string
Theme string
Palette string
// ... per-session-but-not-URL data
}
func SessionOf(w http.ResponseWriter, r *http.Request) *Session {
// Issues an HttpOnly SessionID cookie and stores Session in memory.
...
}
For a real app, swap the in-memory map for whatever persistence layer you already have — the API surface (one method per accessor) keeps the page code unchanged. See Sessions & auth for the integration pattern with real session libraries.
wf.StateOf(r) reads from both the URL query string and the posted form body. When the same key appears in both — typical on a form post that has the page’s existing query string in the URL — the form body wins. That’s what you want: the user’s most recent input takes precedence over the URL that put them on the page.
State written via state.Set(...) lives only in the current request’s view of state; the response either echoes it back as new query parameters (on a redirect) or folds it into the next redraw fragment’s links. The framework handles round-tripping; you don’t write cookies or persistent stores from Set.
Each handler sees only the state encoded in the URL it was called with, plus whatever the form body added. Nothing carries over implicitly between page navigations. If you want a state variable to ride along to the next page, put it in the link:
wf.Link("/orders?filter="+state.Get("filter")).Add("All orders")
For state that should follow the user across every page — theme, locale, login — use a session (above). For state that’s just “return to where I was”, use the _back convention: pass ?_back=<url> when linking into a page, and call wf.RedirectBack(w, r) to return.
When a page is embedded inside another (modal, side panel, named frame), each page has its own state namespace — the framework isolates them. A state variable named q in the modal’s URL does not collide with q on the parent page; they’re independent.
This is what makes EmbedHandler safe to point at any handler: the embedded page renders as if it were on its own URL, with its own query string. State written by the embedded page stays in the embedded page’s URL until the page asks to write back to the parent — that’s what the ^?key= action-URL prefix is for. See Basics → Nesting pages for the full model.
When to redraw — how to react to state moves.
Basics → Incremental updates — the underlying protocol.