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

@yakocloud/state-vocab

v3.1.4

Published

A lightweight React state management library that synchronizes component state with any `Storage`-compatible backend (localStorage, sessionStorage, custom).

Readme

@yakocloud/state-vocab

A lightweight React state management library that synchronizes component state with any Storage-compatible backend (localStorage, sessionStorage, custom).

Why use state-vocab?

Most state managers treat persistence as an afterthought — you manage state first, then manually sync it to localStorage. state-vocab flips this: storage is defined upfront alongside the state itself, and synchronization is automatic.

Storage-first by design. Every state node declares its backend at definition time — localStorage, sessionStorage, or any custom adapter. No useEffect(() => localStorage.setItem(...), [value]) scattered across components.

const storage = setupStorage({
  theme: defineState({ storage: localStorage, defaultValue: 'Dark' }),
  session: defineState({ storage: () => sessionStorage }),
  inMemory: defineState({ defaultValue: 0 }),
})

Bring your own backend. The storage option accepts any object implementing the Web Storage API. Point state directly at a server, IndexedDB wrapper, or encrypted store — no extra adapters needed.

defineState({
  storage: {
    getItem: (key) => api.get(key),
    setItem: (key, value) => api.set(key, value),
    removeItem: (key) => api.delete(key),
    length: 0,
    clear: () => {},
    key: () => null,
  }
})

No prop drilling. State lives in a shared storage object imported directly into any component. No need to pass values down through layers of props or lift state up to a common ancestor.

// ❌ without state-vocab — thread props through every layer
function Page({ theme, onThemeChange }) {
  return <Sidebar theme={theme} onThemeChange={onThemeChange} />
}
function Sidebar({ theme, onThemeChange }) {
  return <ThemeToggle theme={theme} onThemeChange={onThemeChange} />
}
function ThemeToggle({ theme, onThemeChange }) {
  return <button onClick={() => onThemeChange('Dark')}>{theme}</button>
}

// ✅ with state-vocab — import storage, use directly
function ThemeToggle() {
  const [theme, setTheme] = storage.preference.theme.useState()
  return <button onClick={() => setTheme('Dark')}>{theme}</button>
}

Initialize once at the root, read anywhere in the tree. Call .useState() with a defaultValue at a parent component to seed the state — then call .useState() without arguments in any descendant to consume it. No context wiring, no prop passing.

// Root component — initializes the state
function Page() {
  storage.demo.pageProps.useState({
    defaultValue: {
      a: 1,
      b: 2,
      c: ['one', 'two'],
    },
  })

  return <DeepChild />
}

// Somewhere deep in the tree — reads the same state
function DeepChild() {
  const [pageProps] = storage.demo.pageProps.useState()

  return (
    <div>
      <p>a: {pageProps.a}</p>
      <p>b: {pageProps.b}</p>
      <p>c: {pageProps.c.join(', ')}</p>
    </div>
  )
}

Dot-notation access with full TypeScript inference. The state tree is navigated like a plain object — autocomplete guides you to the right node, and types flow from defineState<T> all the way to the hook return value without any manual annotations.

const [theme, setTheme] = storage.preference.theme.useState()
//     ^? 'Dark' | 'White' | 'System'

const [birthday, setBirthday] = storage.personal.birthday.useState()
//     ^? Date | null

If you rename or restructure a node in setupStorage, TypeScript immediately flags every broken reference across the codebase.

Custom serialization per node. Dates, Maps, class instances — define serialize/deserialize once and the hook handles the rest transparently.

defineState({
  storage: localStorage,
  deserialize: (raw) => new Date(JSON.parse(raw)),
})

Minimal API surface. Three exports: defineState, setupStorage, VocabStateProvider. No actions, reducers, selectors, or stores to configure.

Installation

npm install @yakocloud/state-vocab react react-dom

react and react-dom are peer dependencies and must be installed separately.

Quick Start

Wrap your app with VocabStateProvider at the root — this initializes an isolated store for the component tree. Then define and use state anywhere inside it. Library SSR-requirements:

  • Per-request store: A Next.js server can handle multiple requests simultaneously. This means that the store should be created per request and should not be shared across requests.
  • SSR friendly: Next.js applications are rendered twice, first on the server and again on the client. Having different outputs on both the client and the server will result in "hydration errors." The store will have to be initialized on the server and then re-initialized on the client with the same data in order to avoid that.
import { setupStorage, defineState, VocabStateProvider } from '@yakocloud/state-vocab'

type Theme = 'Dark' | 'White' | 'System'

const storage = setupStorage({
  path: {
    to: {
      theme: defineState<Theme>({
        storage: localStorage,
        defaultValue: 'Dark',
      }),
    },
  },
})

function App() {
  return (
    <VocabStateProvider>
      <Settings />
    </VocabStateProvider>
  )
}

function Settings() {
  const [theme, setTheme] = storage.path.to.theme.useState()

  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value as Theme)}>
      <option value="Dark">Dark</option>
      <option value="White">White</option>
      <option value="System">System</option>
    </select>
  )
}

Setup

VocabStateProvider

All components that call .useState() must be descendants of VocabStateProvider. It creates an isolated VocabStore instance for its subtree — multiple providers can coexist in the same app without sharing state.

import { VocabStateProvider } from '@yakocloud/state-vocab'

// Wrap your app root
createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <VocabStateProvider>
      <App />
    </VocabStateProvider>
  </React.StrictMode>
)

You can mount multiple independent providers — each gets its own store:

// Two isolated state trees — state does not bleed between them
<VocabStateProvider>
  <WidgetA />
</VocabStateProvider>

<VocabStateProvider>
  <WidgetB />
</VocabStateProvider>

Core Concepts

defineState(options?)

Defines a state node. Options:

| Option | Type | Description | |---|---|---| | storage | Storage \| (() => Storage) \| undefined | Where to persist the value. Omit for in-memory only. | | defaultValue | T \| (() => T) | Value used when storage has no entry. Accepts a factory function. | | bidirectional | true \| undefined | Sync state across browser tabs via the storage event. | | serialize | (v: T) => string | Custom serializer. Default: JSON.stringify. | | deserialize | (v: string) => T | Custom deserializer. Default: JSON.parse. |

// In-memory (no persistence)
const counter = defineState({ defaultValue: 0 })

// localStorage with custom type
const theme = defineState<'Dark' | 'White'>({
  storage: () => localStorage,
  defaultValue: 'Dark',
})

// localStorage with custom deserialization
const birthday = defineState({
  storage: () => localStorage,
  deserialize: (raw) => new Date(JSON.parse(raw)),
})

// bidirectional — syncs value when changed in another tab
const note = defineState({
  storage: localStorage,
  bidirectional: true,
})

setupStorage(tree, options?)

Wraps a nested object of defineState() nodes and injects dot-separated paths into each leaf. The returned object mirrors your tree structure.

| Option | Type | Description | Default | |---|---|---|---| | verbose | boolean \| undefined | Log current state to the browser console on every change | false | | verbosePath | string \| undefined | Narrow verbose logging to a specific subtree (dot-separated path). When set, only that subtree is logged instead of the entire state. TypeScript will autocomplete valid paths based on your tree. | undefined | | ssr | boolean \| undefined | Defer storage reads until after hydration (Next.js / SSR) | false |

const storage = setupStorage({
  user: {
    name: defineState({ storage: localStorage }),
    age: defineState({ defaultValue: 0 }),
  },
})

storage.user.name  // → path: "user.name"
storage.user.age   // → path: "user.age"

Enable verbose logging during development:

const storage = setupStorage({ ... }, { verbose: true })

Narrow verbose logging to a specific subtree:

const storage = setupStorage({
  user: {
    profile: defineState({ ... }),
    settings: defineState({ ... }),
  },
  cart: {
    items: defineState({ ... }),
  },
}, {
  verbose: true,
  verbosePath: "user", // only logs changes inside "user.*"
})

TypeScript will only accept paths that exist in your tree — "user", "user.profile", "cart.items", etc. Invalid paths are caught at compile time.

SSR / Next.js

When using localStorage or sessionStorage in a Next.js app, the server renders with defaultValue while the client reads the persisted value — causing a hydration mismatch. Pass ssr: true to fix this:

// lib/storage.ts
const storage = setupStorage({
  preference: {
    theme: defineState<Theme>({ storage: localStorage, defaultValue: 'Dark' }),
  },
}, { ssr: true })

With ssr: true:

  • Server & first client render — always use defaultValue, storage is not read
  • After hydrationuseLayoutEffect fires synchronously before paint, reads storage and updates state

This guarantees the server and client produce identical markup, and the value from storage is applied without a visible flash.

Wrap your page or app root with VocabStateProvider as usual — it works correctly in both SSR and client environments.

useState Hook

Each state node exposes a .useState() method that works like React's built-in useState but adds persistence and callbacks.

const [value, setValue, resetValue] = storage.path.to.node.useState(options?)

Options

defaultValue — overrides the defineState-level default for this usage. Accepts a value or factory function:

const [alarm] = storage.alarm.useState({ defaultValue: () => new Date() })

delayedSet — debounces the onSet callback by N milliseconds:

const [note, setNote] = storage.note.useState({
  delayedSet: 1000,
  onSet: (value) => saveToServer(value),
})

onSet — called after every state change with (nextValue, prevValue):

const [counter, setCounter] = storage.counter.useState({
  onSet(next, prev) {
    console.log(`Changed from ${prev} to ${next}`)
  },
})

bidirectional — enables cross-tab sync via the storage event. Can be set here or in defineState:

const [note, setNote] = storage.note.useState({ bidirectional: true })

Return value

const [value, setValue, resetValue] = storage.node.useState()
  • value — current state
  • setValue(nextValue | updater) — set state; accepts a value or (prev) => next function
  • resetValue() — restores the default value (removes the storage entry if no default is defined)

Custom Storage

Any object implementing the Web Storage API works as a backend. This makes it easy to add debouncing, encryption, or remote persistence:

const db: Record<string, string> = {}

const storage = setupStorage({
  server: {
    data: defineState({
      defaultValue: '',
      storage: {
        length: 0,
        getItem: (key) => db[key] ?? null,
        setItem: (key, value) => debouncedSave(key, value),
        removeItem: (key) => { delete db[key] },
        clear: () => {},
        key: () => null,
      },
    }),
  },
})

Full Example

import React from 'react'
import { createRoot } from 'react-dom/client'
import { setupStorage, defineState, VocabStateProvider } from '@yakocloud/state-vocab'

type Theme = 'Dark' | 'White' | 'System'

const storage = setupStorage({
  preference: {
    theme: defineState<Theme>({ storage: localStorage, defaultValue: 'Dark' }),
    nightMode: defineState({ storage: sessionStorage, defaultValue: false }),
  },
  stats: {
    counter: defineState({ defaultValue: 0, storage: sessionStorage }),
  },
  personal: {
    note: defineState({ storage: localStorage, defaultValue: '' }),
    birthday: defineState({
      storage: localStorage,
      deserialize: (raw) => {
        try { return new Date(JSON.parse(raw)) } catch { return null }
      },
    }),
  },
  demo: {
    pageProps: defineState<{ title: string; count: number }>({
      storage: localStorage,
    }),
  },
})

// Root — initializes shared state for the whole subtree
function Page() {
  storage.demo.pageProps.useState({
    defaultValue: { title: 'Hello', count: 42 },
  })

  return <Dashboard />
}

// Deep child — reads without re-specifying defaults
function PageHeader() {
  const [pageProps] = storage.demo.pageProps.useState()
  return <h1>{pageProps.title} ({pageProps.count})</h1>
}

function Dashboard() {
  const [theme, setTheme] = storage.preference.theme.useState()
  const [nightMode, setNightMode] = storage.preference.nightMode.useState()
  const [counter, setCounter, resetCounter] = storage.stats.counter.useState()
  const [note, setNote] = storage.personal.note.useState({
    delayedSet: 500,
    onSet: (v) => console.log('Saving note:', v),
  })

  return (
    <div>
      <PageHeader />

      <select value={theme ?? ''} onChange={(e) => setTheme(e.target.value as Theme)}>
        <option value="Dark">Dark</option>
        <option value="White">White</option>
        <option value="System">System</option>
      </select>

      <input
        type="checkbox"
        checked={!!nightMode}
        onChange={(e) => setNightMode(e.target.checked)}
      />

      <input
        type="number"
        value={counter}
        onChange={(e) => setCounter(+e.target.value)}
      />
      <button onClick={resetCounter}>Reset</button>

      <input
        type="text"
        value={note}
        onChange={(e) => setNote(e.target.value)}
      />
    </div>
  )
}

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <VocabStateProvider>
      <Page />
    </VocabStateProvider>
  </React.StrictMode>
)

API Reference

defineState<T>(options?)

| Option | Type | Default | |---|---|---| | storage | Storage \| (() => Storage) \| undefined | undefined (in-memory) | | defaultValue | T \| (() => T) \| undefined | undefined | | bidirectional | true \| undefined | undefined | | serialize | (v: T) => string | JSON.stringify | | deserialize | (v: string) => T | JSON.parse |

setupStorage<T>(tree: T, options?): T

| Option | Type | Default | |---|---|---| | verbose | boolean \| undefined | false | | verbosePath | Path<T> \| undefined | undefined | | ssr | boolean \| undefined | false |

Returns a proxied copy of tree with paths injected into all leaf nodes.

VocabStateProvider

A React context provider that initializes a VocabStore for its subtree. Required — all components calling .useState() must be descendants of this provider.

<VocabStateProvider>
  <App />
</VocabStateProvider>

node.useState(options?)

| Option | Type | Description | |---|---|---| | defaultValue | T \| (() => T) \| undefined | Local default, overrides defineState default | | delayedSet | number \| undefined | Debounce delay for onSet in ms | | onSet | (next: T, prev: T) => void \| undefined | Callback after state change | | bidirectional | true \| undefined | Sync state across browser tabs |

Returns [value, setValue, resetValue]