@sovgut/state
v2.2.0
Published
Type-safe, synchronous state management for the browser with a unified API over localStorage, sessionStorage, cookies, and in-memory storage, plus a built-in observer for change events.
Maintainers
Readme
@sovgut/state
Type-safe, synchronous state management for the browser with a unified API over localStorage, sessionStorage, cookies, and in-memory storage. Each backend exposes the same interface and an isolated observer for change notifications.
Contents
- Installation
- Overview
- Storage backends
- Reading and writing values
- Type casting
- Observing changes
- React integration
- API reference
- Error handling
- License
Installation
npm install @sovgut/stateyarn add @sovgut/statepnpm add @sovgut/stateThe package ships as ES modules with bundled type declarations and has no runtime dependencies.
Overview
import { LocalState } from "@sovgut/state";
LocalState.set("user", { id: 1, name: "Alice" });
const user = LocalState.get("user");
LocalState.on("user", (value) => {
console.log("user changed:", value);
});LocalState, SessionState, MemoryState, and CookieState are static classes. There is nothing to instantiate; import a backend and call its methods directly.
Storage backends
All backends share the same core interface. They differ in where data is stored and how long it survives.
| Backend | Storage | Lifetime |
| -------------- | ------------------- | ------------------------------------- |
| LocalState | localStorage | Persists across browser sessions |
| SessionState | sessionStorage | Cleared when the tab is closed |
| MemoryState | In-memory Map | Cleared on page reload |
| CookieState | document.cookie | Configurable via cookie attributes |
LocalState
import { LocalState } from "@sovgut/state";
LocalState.set("theme", "dark");
const theme = LocalState.get("theme"); // "dark"SessionState
import { SessionState } from "@sovgut/state";
SessionState.set("draft", { subject: "", body: "" });
const draft = SessionState.get("draft");MemoryState
Values are stored by reference without a JSON roundtrip, so Date, Map, Set, and class instances survive a get/set cycle unchanged. Stored objects are not defensively copied.
import { MemoryState } from "@sovgut/state";
MemoryState.set("cache", new Map([["key", "value"]]));
const cache = MemoryState.get<Map<string, string>>("cache");CookieState
set accepts standard cookie attributes. path defaults to / and sameSite defaults to lax. When sameSite is none, secure is enabled automatically.
import { CookieState } from "@sovgut/state";
CookieState.set("sessionId", "abc123", {
expires: 7, // days from now, or a Date
secure: true,
sameSite: "strict",
});Cookies set with a custom path or domain must be removed with the same values, because browsers require an exact match to delete a cookie:
CookieState.set("token", value, { path: "/admin" });
CookieState.remove("token", { path: "/admin" });Reading and writing values
Writing
Values written to LocalState, SessionState, and CookieState are JSON-serialized. A value that cannot be serialized — undefined, a function, a symbol, or a circular reference — causes set to throw.
LocalState.set("count", 42);
LocalState.set("name", "Alice");
LocalState.set("isActive", true);
LocalState.set("tags", ["typescript", "state"]);
LocalState.set("settings", {
theme: "dark",
notifications: { email: true, push: false },
});Reading
get returns the stored value, or undefined when the key is absent.
const count = LocalState.get("count"); // 42
const missing = LocalState.get("nonexistent"); // undefinedProvide a fallback to return a default when the key is absent or holds an empty value. The fallback also drives the inferred return type.
const theme = LocalState.get("theme", { fallback: "light" }); // string
const score = LocalState.get("score", { fallback: 0 }); // number
const tags = LocalState.get("tags", { fallback: [] as string[] }); // string[]Use strict to throw StateDoesNotExist when the key is absent.
try {
const token = LocalState.get("token", { strict: true });
} catch (error) {
// key was not present
}Removing and checking
LocalState.remove("user"); // remove one key
LocalState.clear(); // remove all keys for this backend
LocalState.has("user"); // booleanType casting
Provide a cast option to coerce a stored value to a primitive type. Supported targets are "string", "number", "boolean", and "bigint".
LocalState.set("value", "42");
LocalState.get("value", { cast: "string" }); // "42"
LocalState.get("value", { cast: "number" }); // 42
LocalState.get("value", { cast: "boolean" }); // true
LocalState.get("value", { cast: "bigint" }); // 42nAn impossible cast — for example "abc" to number — returns the fallback when one is provided, or throws StateInvalidCast when strict is set. Boolean casting treats "false", "0", and "" as false.
A generic parameter can be supplied to type the return value directly:
interface User {
id: number;
name: string;
}
const user = LocalState.get<User>("currentUser");
const required = LocalState.get<User>("currentUser", { strict: true });Observing changes
Each backend maintains its own listener registry, so LocalState listeners do not fire on SessionState events. Listeners receive the new value, or null when a key is removed or cleared.
const handler = (value) => console.log("changed:", value);
LocalState.on("user", handler); // subscribe
LocalState.once("user", handler); // fire at most once
LocalState.off("user", handler); // unsubscribeNote: storing an explicit null via set emits null as well, so a listener cannot distinguish "set to null" from "removed" by the payload alone.
Additional inspection and cleanup methods are available:
LocalState.listenerCount("user"); // number of listeners for a key
LocalState.eventNames(); // keys with at least one listener
LocalState.removeListener("user"); // remove all listeners for one key
LocalState.removeAllListeners(); // remove every listener on the backendA listener that throws is isolated: the error is logged and other listeners still run.
React integration
The library is framework-agnostic. A minimal hook that keeps component state synchronized with a backend:
import { useCallback, useEffect, useState } from "react";
import { LocalState, type IStorageEventData } from "@sovgut/state";
function useLocalState<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() =>
LocalState.get(key, { fallback: initialValue }),
);
useEffect(() => {
const handler = (event: IStorageEventData<T>) => {
setValue(event ?? initialValue);
};
LocalState.on(key, handler);
return () => LocalState.off(key, handler);
}, [key, initialValue]);
const update = useCallback(
(next: T) => LocalState.set(key, next),
[key],
);
return [value, update] as const;
}
function ThemeToggle() {
const [theme, setTheme] = useLocalState("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme}
</button>
);
}API reference
The methods below are shared by all four backends unless noted otherwise.
get<T>(key, options?): T | undefined
Retrieves a value from storage.
| Option | Type | Description |
| ---------- | --------------------------------------------- | ---------------------------------------------------- |
| fallback | T | Returned when the key is absent or holds an empty value. |
| strict | boolean | Throw StateDoesNotExist when the key is absent. |
| cast | "string" \| "number" \| "boolean" \| "bigint" | Coerce the stored value to a primitive type. |
set<T>(key, value): void
Stores a value. Throws when the value cannot be serialized or the backend rejects the write. CookieState.set(key, value, options?) additionally accepts cookie attributes:
| Option | Type | Description |
| ---------- | -------------------------------- | ------------------------------------------------------- |
| expires | Date \| number | Expiration date, or number of days from now. |
| maxAge | number | Maximum age in seconds. Takes precedence over expires.|
| domain | string | Domain the cookie is scoped to. |
| path | string | Path the cookie is scoped to. Defaults to /. |
| secure | boolean | Send only over HTTPS. Required when sameSite is none. |
| sameSite | "strict" \| "lax" \| "none" | SameSite policy. Defaults to lax. |
remove(key): void
Removes a value. CookieState.remove(key, options?) accepts path and domain, which must match the values used when the cookie was set.
clear(): void
Removes every value for the backend and emits null for each removed key.
has(key): boolean
Returns whether a key exists.
Observer methods
| Method | Description |
| ----------------------------- | ------------------------------------------------ |
| on(event, callback) | Add a listener. |
| once(event, callback) | Add a listener that fires at most once. |
| off(event, callback) | Remove a specific listener. |
| removeListener(event) | Remove all listeners for one key. |
| removeAllListeners() | Remove every listener on the backend. |
| listenerCount(event) | Number of listeners registered for a key. |
| eventNames() | Keys that have at least one listener. |
Error handling
Both error types carry structured fields for programmatic handling.
import { StateDoesNotExist, StateInvalidCast } from "@sovgut/state";
try {
LocalState.get("token", { strict: true });
} catch (error) {
if (error instanceof StateDoesNotExist) {
console.error(`"${error.key}" not found in ${error.storage}`);
}
}
try {
LocalState.set("value", "not-a-number");
LocalState.get("value", { cast: "number", strict: true });
} catch (error) {
if (error instanceof StateInvalidCast) {
console.error(
`cannot cast "${error.value}" to ${error.type} for "${error.key}"`,
);
}
}License
Released under the MIT License.
