npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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.

npm version npm downloads license

Contents

Installation

npm install @sovgut/state
yarn add @sovgut/state
pnpm add @sovgut/state

The 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"); // undefined

Provide 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");      // boolean

Type 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" });  // 42n

An 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);  // unsubscribe

Note: 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 backend

A 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.