State
useState(key, init) is state that is shared by key and travels with the page. The server creates one store per request, writes it into the payload after rendering, and the browser restores it before hydration, so a value set during server rendering is the value the browser starts with.
import { useState as useSharedState } from "@silgi/react";
export default function Counter() {
const [count, setCount] = useSharedState("count", 0);
return (
<button type="button" onClick={() => setCount((value) => value + 1)}>
Clicked {count} times
</button>
);
}
- Without a key,
useState(() => 1)gets one from the build, one per call site (build.keyedComposables, Configuration); a string as the first argument is always the key. - The key comes first, unlike React's
useState.@silgi/reactand React both exportuseState, so in a file that needs both, import one under another name, as the sample does. - Every component that uses the same key reads and writes the same value.
initis a value or a function. It runs only while the key has no value;undefinedcounts as a value.- A write notifies the key's subscribers only when the value changed (
Object.is). - The value must be serializable, with the same rules as loader data: see Data.
- Nothing is shared between requests on the server. Do not keep per-user data in a module-level variable; keep it here.
The return value follows the library: React and Preact return [value, set], Solid [accessor, set], Vue a Ref, and Svelte and html an object with value.
Route middleware gets the same store as ctx.state, so a middleware can check or set a value before the page renders.
Clearing, once-only work and config
import { callOnce, updateAppConfig, useApp, useState } from "@silgi/react";
import { $fetch, clearState } from "silgi/app";
export default function Cart() {
const app = useApp();
const [items] = useState<string[]>("cart", () => []);
void callOnce("cart-visit", () => $fetch("/api/visits", { method: "POST" }));
return (
<div>
<p>{items.join(", ")}</p>
<button type="button" onClick={() => clearState(app, "cart")}>Empty</button>
<button type="button" onClick={() => clearState(app, (key) => key.startsWith("cart"), { reset: false })}>Forget</button>
<button type="button" onClick={() => updateAppConfig({ theme: { color: "teal" } })}>Teal</button>
</div>
);
}
clearState(keys?, { reset = true })puts state keys back to theinittheiruseStategave.keysis a key, a list or a function of the key; without it every key is matched. Withreset: falsethe keys becomeundefined. A key that holds nothing is skipped. The adapter'sclearStateandcallOncecalluseApp(), so call them while the component renders. In an event handler, readconst app = useApp()during render and callclearState(app, keys)fromsilgi/app, as the sample does. It does not touch async data;clearAsyncDatadoes (Data).callOnce(key, fn, { mode })runsfnonce forkeyand returns a promise. The default mode"render"runs it once per page load: the server runs it, and the browser skips every key the server ran, in hydration and in later renders.mode: "navigation"runs it once per request on the server and once per client navigation; the first navigation after hydration runs it again, a refresh of the same page does not. Calls of one key whilefnruns share its promise. Iffnthrows, the key is not done and the next call runs it again.updateAppConfig(patch)mergespatchinto the app config (Configuration) and tells everyuseAppConfig()reader. Plain objects merge; arrays and other values replace. With the app on the server it changes that request's copy only, and the page payload carries it, so one request never sees another's change. On the server, pass the app (updateAppConfig(useApp(), patch)in a component,updateAppConfig(app, patch)elsewhere); without it the call throws there.
Handing a value from the server to the browser
useState is the way for a component. A plugin that owns a value outside the store, such as the start of a session or a preference object, uses useHydration(app, key, get, set). The server calls get() after the render and writes the result into the page; the browser calls set(value) while the plugin runs, before the first render.
import { definePlugin, useHydration } from "silgi/app";
export default definePlugin((app) => {
const prefs = { locale: "en", startedAt: 0 };
useHydration(
app,
"prefs",
() => prefs,
(value) => Object.assign(prefs, value),
);
return { provide: { prefs } };
});
The value is serialized like the rest of the page, custom types included. Nothing is called on a client navigation, and a page that is not server-rendered has no value. A key used twice in one app fails with useHydration key "prefs" is already used.
Cookies
useCookie(name, options) has the same shape as useState but its value is a cookie. On the server it reads the request's cookie header and writes set-cookie on the response; in the browser it reads and writes document.cookie.
import { useCookie } from "@silgi/react";
export default function ThemeSwitch() {
const [theme, setTheme] = useCookie("theme", { maxAge: 60 * 60 * 24 * 365 });
return (
<button type="button" onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Theme: {theme ?? "light"}
</button>
);
}
Values are JSON by default, so useCookie<{ theme: string }>("prefs") holds an object; a cookie that is not JSON reads as its string. default, encode, decode and readonly are in Adapters. set(undefined) deletes the cookie. The options are those of setCookie in Server; path defaults to /. A cookie that is httpOnly is not visible in the browser. A cookie written after a streamed response has sent its headers is lost, so on a streamed page set the cookie in server middleware.
Where the browser has cookieStore (app.cookieStore, on by default), a cookie another tab wrote reaches useCookie by itself. Code outside the app that writes document.cookie tells the app with refreshCookie(name), which re-reads the cookie and notifies the useCookie(name) readers whose value changed. It does nothing on the server, and it cannot read an HttpOnly cookie. In Vue, useCookie(name, { watch }) writes the cookie when the ref changes; see Adapters.