Browser-backed reactive state for URL and localStorage.
browser(options?) returns:
path: reactive string bound tolocation.pathnamequery: reactive record bound tolocation.searchhash: reactive record bound tolocation.hashlocal(key, dflt, normalizerOrSerializer?, opts?):localStorage-backed cellinternal(name, value): in-memory shared cell registry for cross-component stateref(value): resolves@,#, and?cell references, including:selectionsval(value): parses booleans and hashformat-like text, leaving reference strings unchangedparse(value): compatibility parser that dispatches toref(value) ?? val(value)fetch(input, options?): fetch helper with typed response parsingfetched(input, options?): reactive cell wrapper aroundfetch(input, options?)
@./browser.js also exports Browser, the class used by browser(options?).
import browser from "@./browser.js"
const state = browser()
state.path.set("/docs")
state.query.set({ page: 2, filter: "active" })
state.hash.set({ section: "api" })
const prefs = state.local("prefs", { theme: "light" })
prefs.select("theme").set("dark")
const modal = state.internal("modal.open", false)
modal.set(true)
const current = state.ref("@modal.open")
const currentName = state.ref("@session.user:profile.name")
const draft = state.val("title=Draft,done=F")
const fallback = state.parse("true")
const result = await state.fetch("POST:/api/items#label=Draft,done=F")
const resultCell = state.fetched("POST:/api/items#label=Draft,done=F")@./browser.js also exports reusable serializers:
record:{ parse, format }sanitizer for plain recordsquery:{ parse, format }serializer forlocation.searchhash:{ parse, format }serializer forlocation.hash
These can be passed anywhere the code accepts a serializer object.
pathuses browser path encodingqueryandhashuse the Select hashformat syntax by defaultlocaldefaults to JSON
By default, query and hash both use the same hashformat payload syntax:
- lists:
1,2,3 - objects:
a=1,b=2 - nested values:
a=(1,2),b=(x=1,y=2)
Notes:
- query strings may start with
? - hash fragments may start with
# - query parsing ignores any trailing
#fragment - legacy
a=1&b=2query syntax is not supported by default - hash parsing treats a bare first value as a
pathkey; see Browser Reference for details
- invalid values are sanitized before they are written
- unsafe keys such as
__proto__,prototype, andconstructorare pruned - control characters are removed from serialized text
- returns
undefinedfor non-string or non-reference inputs @nameresolves tointernal("name")@name.path.to.valueresolves tointernal("name").select(["path", "to", "value"])@name.with.dots:path.to.valueresolves tointernal("name.with.dots").select(["path", "to", "value"])#nameresolves tohash.select(["name"])#name.path.to.valueresolves tohash.select(["name", "path", "to", "value"])#name.with.dots:path.to.valueresolves tohash.select(["name.with.dots"]).select(["path", "to", "value"])?nameresolves toquery.select(["name"])?name.path.to.valueresolves toquery.select(["name", "path", "to", "value"])?name.with.dots:path.to.valueresolves toquery.select(["name.with.dots"]).select(["path", "to", "value"])- when
:is present, everything before:is treated as the full cell name and everything after:is the nested selection path - numeric dotted segments become indexes, so
#users.0.nameresolves tohash.select(["users", 0, "name"]) - numeric dotted segments after
:also become indexes, so?users:list.0.nameresolves to `query.select(["users"]).select(["list", 0, "name"])
- non-string values are returned unchanged
"true"becomestrue"false"becomesfalse- text containing hashformat structure such as
=,,,(,), or a leading#is parsed with the default hash parser - reference strings such as
@modal.openare returned unchanged - plain text such as
helloor42is returned unchanged
- dispatches to
ref(value)first - falls back to
val(value)when the input is not a browser reference - preserves the previous mixed behavior for existing callers
When input matches METHOD:PATH?QUERY#DATA:
METHODbecomes the fetch methodPATH?QUERYbecomes the request URLDATAis parsed as hashformat and JSON-encoded into the request bodycontent-type: application/jsonis added when a body is generated and no content type is already set
Responses are normalized by content type:
- JSON content types return parsed JSON
- text-like content types return
text() - everything else returns a
Blob
Returns a cell that resolves to the same normalized result as fetch(input, options?).
When no window is available, browser() still returns the same interface,
but it behaves as inert in-memory state.