@dvashim/store
v1.5.1
Published
Yet another state management in React
Maintainers
Readme
@dvashim/store
A minimal, lightweight React state management library built on useSyncExternalStore.
Install
npm:
npm install @dvashim/storeor pnpm:
pnpm add @dvashim/storePeer dependencies: react >= 18
Quick Start
import { createStore, useStore } from '@dvashim/store'
const count$ = createStore(0)
function Counter() {
const count = useStore(count$)
return (
<button onClick={() => count$.update((n) => n + 1)}>
Count: {count}
</button>
)
}API
createStore(initialState?)
Creates a new Store instance.
const count$ = createStore(0)
const user$ = createStore({ name: 'Alice', age: 30 })
// Without initial state — type defaults to T | undefined
const data$ = createStore<string>()Store
Reactive state container with subscription-based change notification.
store.get()
Returns the current state.
const count$ = createStore(10)
count$.get() // 10store.set(state, options?)
Replaces the state. Skipped if the value is identical (Object.is), unless { force: true } is passed.
count$.set(5)
// Force notify subscribers even if the value hasn't changed
count$.set(5, { force: true })store.update(updater, options?)
Derives the next state via an updater function. Calling set() or update() from within a subscriber throws an error.
count$.update((n) => n + 1)
// With objects — always return a new reference
const todos$ = createStore([{ text: 'Buy milk', done: false }])
todos$.update((todos) => [...todos, { text: 'Walk dog', done: false }])store.subscribe(fn)
Registers a callback invoked on each state change with the new and previous state. Returns an unsubscribe function.
const unsubscribe = count$.subscribe((state, prevState) => {
console.log(`Count changed from ${prevState} to ${state}`)
})
// Later...
unsubscribe()ComputedStore
A read-only reactive store that derives its value from a source store using a selector. Automatically updates when the source changes. Accepts any SourceStore<T> (including Store or another ComputedStore) as its source.
import { createStore, ComputedStore } from '@dvashim/store'
const todos$ = createStore([
{ text: 'Buy milk', done: true },
{ text: 'Walk dog', done: false },
])
const remaining$ = new ComputedStore(todos$, (todos) =>
todos.filter((t) => !t.done).length
)
remaining$.get() // 1
remaining$.subscribe((count, prev) => console.log(`${prev} → ${count}`))Chaining
ComputedStore implements SourceStore<U>, so it can be used as the source for another ComputedStore.
const count$ = new ComputedStore(todos$, (todos) => todos.length)
const label$ = new ComputedStore(count$, (n) => `${n} items`)
label$.get() // "2 items"computed.connect() / computed.disconnect()
Control the subscription to the source store. After disconnect(), the derived value stops updating and get() returns the last known value. Call connect() to resume — it immediately syncs the derived value with the current source state before resubscribing.
remaining$.disconnect()
remaining$.isConnected // false
remaining$.connect()
remaining$.isConnected // trueuseStore(store, selector?)
React hook that subscribes a component to any SourceStore — works with both Store and ComputedStore.
function Counter() {
const count = useStore(count$)
return <p>{count}</p>
}
function Remaining() {
const remaining = useStore(remaining$)
return <p>{remaining} left</p>
}With a selector
Derive a value from the store state. The selector should return a referentially stable value (primitive or existing object reference) to avoid unnecessary re-renders.
const user$ = createStore({ name: 'Alice', age: 30 })
function UserName() {
const name = useStore(user$, (user) => user.name)
return <p>{name}</p>
}Types
The following types are exported from the package:
import type { Selector, Subscriber } from '@dvashim/store'| Type | Definition |
| ---- | ---------- |
| Selector<T, U> | (state: T) => U |
| Subscriber<T> | (state: T, prevState: T) => void |
Patterns
Shared stores across components
Define stores outside of components and import them where needed.
// stores/auth.ts
import { createStore } from '@dvashim/store'
export const token$ = createStore<string | null>(null)
export function login(token: string) {
token$.set(token)
}
export function logout() {
token$.set(null)
}// components/Profile.tsx
import { useStore } from '@dvashim/store'
import { token$, logout } from '../stores/auth'
function Profile() {
const token = useStore(token$)
if (!token) return <p>Not logged in</p>
return <button onClick={logout}>Log out</button>
}Combining multiple stores
import { createStore, useStore } from '@dvashim/store'
const firstName$ = createStore('Alice')
const lastName$ = createStore('Smith')
function FullName() {
const firstName = useStore(firstName$)
const lastName = useStore(lastName$)
return <p>{firstName} {lastName}</p>
}Using the Store class directly
import { Store } from '@dvashim/store'
class TimerService {
readonly seconds$ = new Store(0)
#interval: ReturnType<typeof setInterval> | undefined
start() {
this.#interval = setInterval(() => {
this.seconds$.update((s) => s + 1)
}, 1000)
}
stop() {
clearInterval(this.#interval)
}
}License
MIT
