@oleksandr_leskiv/use-shared-state
v1.0.2
Published
Fully-typed shared state for React with `useState` semantics.
Readme
use-shared-state
Fully-typed shared state for React with useState semantics.
What is this?
use-shared-state is a small, React-only hook library that lets you share state
between unrelated components with the same API and mental model as React’s
useState.
It supports both in-memory shared state and optional, per-key persistence
to browser storages such as localStorage and sessionStorage, with support for
custom storage backends and custom serialization.
State is shared by globally typed keys, ensuring that:
- every key has exactly one type across the entire app
- all consumers stay automatically synchronized
- no reducers, actions, or selectors are needed
Quick example
Define a shared key once:
declare module 'use-shared-state' {
export interface SharedKeys {
apiToken: string | null
}
}Update shared state in one component:
import { useSharedState } from 'use-shared-state'
function LoginScreen() {
const [, setApiToken] = useSharedState('apiToken')(null)
return (
<button onClick={() => setApiToken('secret-token')}>
Login
</button>
)
}Consume the same state somewhere else:
function UserProfile() {
const [apiToken] = useSharedState('apiToken')()
return apiToken
? <span>Authenticated</span>
: <span>Not authenticated</span>
}Table of Contents
- What it gives you
- Installation
- Usage
- TypeScript experience
- Update semantics
- Limitations
- Motivation and comparison
- FAQ
- License
What it gives you
useState-compatible API (useSharedState(key)returns the same tuple)- Fully typed shared keys via TypeScript module augmentation
- Automatic synchronization across components using the same key
- Optional scoping via
Provider - Optional per-key persistence to
localStorage,sessionStorage, or custom storage backends - Dispatch-only hook to avoid rerenders when you only need to update state
Installation
npm install use-shared-statepnpm add use-shared-stateyarn add use-shared-stateUsage
import { useSharedState } from 'use-shared-state'useSharedState(key) returns a useState-like hook. It accepts:
- a default value:
useSharedState('key')(defaultValue) - an initializer function:
useSharedState('key')(() => defaultValue) - nothing:
useSharedState('key')()(value will beT | undefined)
Initialization and synchronization rules
For each key, the first component to render decides the initial value:
- If persistence is enabled and a stored value exists, it is restored.
- Otherwise, if a default value / initializer was provided, it is used.
- Otherwise, the value remains
undefined.
If the first usage results in undefined, a later usage may still provide a
default and initialize the key. Once initialized, the value is shared and kept
in sync for all consumers.
Provider
By default, you do not need a Provider at all.
If you don’t render Provider, use-shared-state uses a single global store for
the entire application.
Use Provider only when you want to isolate shared state for a subtree.
import { Provider } from 'use-shared-state'
function App() {
return (
<Provider>
<Feature />
</Provider>
)
}Nested providers create independent stores.
Persistence
Persistence is configured per key via the Provider's storeConfig prop.
If you don’t need persistence, you don’t need to configure anything.
<Provider
storeConfig={{
persist: {
apiToken: true, // localStorage
language: sessionStorage, // custom storage
complex: {
storage: true,
customEncoding: { encode, decode },
},
},
}}
>
<Root />
</Provider>Rules:
true→localStorage- storage-like object → that storage
falseor missing key → persistence disabled- values are JSON-encoded by default
customEncodingallows custom serialization- setting a value to
nullremoves the persisted entry
Dispatch-only hook
Use useSharedStateDispatch if you only need to update state and want to avoid
rerenders:
import { useSharedStateDispatch } from 'use-shared-state'
function LogoutButton() {
const setApiToken = useSharedStateDispatch('apiToken')
return <button onClick={() => setApiToken(null)}>Logout</button>
}TypeScript experience
This library is designed to be TypeScript-first.
- Keys must exist in
SharedKeys - Each key has exactly one type across the entire app
- Autocomplete works for keys and values
- Using an unknown key or wrong type is a compile-time error
This design intentionally prevents subtle runtime bugs caused by inconsistent shared state shapes.
Update semantics
setState behaves exactly like React’s useState setter:
- accepts a value or an updater function
- no automatic merging
- updates always notify all subscribers, even if the value is unchanged
Limitations
- React-only
- Uses browser storage APIs for persistence
- Not SSR-safe by default (client-only usage recommended)
Motivation and comparison
use-shared-state is designed for shared client-side state with the same ergonomics as useState, plus optional persistence.
At a glance
| Tool | What it’s best for | Key trade-off |
|---|---|---|
| useState | Local component state | Not shareable |
| useContext | Global values / dependencies | Centralized ownership |
| useReducer | Complex state transitions | Boilerplate (actions/reducers) |
| use-between | Sharing state via custom hooks | Requires a dedicated hook per entity |
| Zustand / Jotai | Full app state management | Extra abstraction |
| use-shared-state | Shared state, writable from anywhere | Key-based, not hook-based |
One-sentence summary
useContext→ global values, usually owned by a provideruseReducer→ shared state via actions and reducersuse-between→ share state by creating and reusing a hook per entity- Zustand / Jotai → external stores for broader state management
- use-shared-state →
useState, but shared by typed keys, writable from anywhere, optionally persistent
FAQ
Why are generics not supported?
To ensure the same key cannot be used with different types in different parts of the application.
Why is my value undefined?
Because no persisted value exists and no default value has been provided yet.
Why didn’t my default value apply?
Once a key is initialized, later defaults are ignored.
License
MIT
