Reactive state management for React, Vue, Svelte, Solid, and Angular — one store, shared across frameworks. Inspired by Recoil and Jotai. The framework-agnostic core also runs in plain JavaScript, Node, and workers.
Docs: valdres.dev · AI-readable: llms.txt (every page is also available as markdown — append .md to any URL)
npm install valdres valdres-react # or valdres-vue / -svelte / -solid / -angularimport { atom, selector } from "valdres"
import { Provider, useAtom, useValue } from "valdres-react"
const countAtom = atom(0)
const doubledSelector = selector(get => get(countAtom) * 2)
function Counter() {
const [count, setCount] = useAtom(countAtom)
const doubled = useValue(doubledSelector)
return <button onClick={() => setCount(c => c + 1)}>{count} ×2 = {doubled}</button>
}
const App = () => (
<Provider>
<Counter />
</Provider>
)Atoms and selectors are identified by reference (no string keys), families are first-class, transactions batch updates, and scoped stores fork state for edit-and-cancel flows. The same atoms work in every framework — and in the plugin packages that wrap browser APIs (geolocation, keyboard, visibility, …) as reactive state.
The package tables below are auto-generated — do not hand-edit.
| Package | Description |
|---|---|
valdres |
Fast atom-based state management for JavaScript. Inspired by Recoil and Jotai. |
| Package | Description |
|---|---|
valdres-angular |
Angular bindings for Valdres state management |
valdres-react |
React bindings for Valdres state management |
valdres-solid |
Solid.js bindings for Valdres state management |
valdres-svelte |
Svelte 5 bindings for Valdres state management |
valdres-vue |
Vue 3 bindings for Valdres state management |
| Package | Description |
|---|---|
@valdres/bandwidth |
Reactive network throughput, latency, and jitter from a live measurement |
@valdres/browser-color-scheme |
Reactive prefers-color-scheme (dark / light) media query |
@valdres/browser-contrast |
Reactive prefers-contrast preference from the OS |
@valdres/browser-device-motion |
Reactive device acceleration and rotation from DeviceMotionEvent |
@valdres/browser-device-orientation |
Reactive device tilt and compass heading from DeviceOrientationEvent |
@valdres/browser-focus |
Reactive document focus state from the window focus/blur events |
@valdres/browser-geolocation |
Reactive geolocation — position, accuracy, permission, and status |
@valdres/browser-keyboard |
Reactive pressed-key state from keydown / keyup |
@valdres/browser-online |
Reactive online/offline status from navigator.onLine |
@valdres/browser-presence |
Whether the user is present — tab visible and window focused |
@valdres/browser-reduced-data |
Reactive prefers-reduced-data media query |
@valdres/browser-reduced-motion |
Reactive prefers-reduced-motion preference from matchMedia |
@valdres/browser-reduced-transparency |
Reactive prefers-reduced-transparency media query state |
@valdres/browser-screen |
Reactive screen resolution and orientation from window.screen |
@valdres/browser-screen-details |
Reactive multi-screen layout from the Window Management API |
@valdres/browser-visibility |
Reactive Page Visibility state (visible / hidden) |
@valdres/browser-window |
Reactive window inner size, tracked through resize events |
@valdres/color-mode |
Color mode (dark/light theme) state powered by Valdres |
@valdres/hotkeys |
Hotkey state management powered by Valdres |
@valdres/public-ip |
Reactive public IP (v4/v6) with stale-while-revalidate |
@valdres/redux-devtools |
Connect a valdres store to the Redux DevTools browser extension |
| Package | Description |
|---|---|
@valdres-react/color-mode |
React color mode hooks powered by Valdres |
@valdres-react/draggable |
React drag-and-drop powered by Valdres |
@valdres-react/hotkeys |
React hotkey hooks powered by Valdres |
@valdres-react/jotai |
Jotai API compatibility layer for Valdres |
@valdres-react/panable |
React pan and zoom powered by Valdres |
@valdres-react/recoil |
Recoil API compatibility layer for Valdres |
bun install
bun test # all packages
bun run docs:dev # docs site at localhost:4321Versioning and publishing is handled by Changesets. Each package versions independently.
When you open a PR that changes a publishable package:
bunx changesetPick the affected packages, the bump type (patch/minor/major), and write a short summary. Commit the generated .changeset/*.md file with your PR.
For PRs that touch publishable code but intentionally don't trigger a release (refactors, internal cleanup, docs):
bunx changeset --emptyThis still generates a .changeset/*.md file — commit it like a regular changeset. The Require changeset check on each PR enforces that any change to a publishable package ships with a changeset (empty or otherwise).
When the PR merges to main, the Publish workflow opens (or updates) a Version Packages PR that applies the pending changesets, bumps versions, and updates CHANGELOGs. Merging that PR publishes the affected packages to npm.
To preview what publishing would do locally:
bun run verify-publishThe repo is currently in beta prerelease mode (bunx changeset pre exit to graduate to stable).
valdres is benchmarked against Jotai (and a raw Map floor) on every PR via Bencher — live, always-current latency per operation under both Bun (JavaScriptCore) and Node.js (V8):
Live plot — store.get(atom) latency, valdres vs Jotai vs a raw Map floor, on both runtimes. Auto-updates from main; click through to filter/zoom. (Sparse until main accumulates a few runs.)
Every PR from the repo gets a comment flagging any latency regression vs main (fork PRs are skipped — they can't read the upload key).
Latest main latency per operation (live, always-current numbers: bencher.dev/perf/valdres). Auto-generated from Bencher — do not hand-edit.
| Operation | valdres | Jotai | |
|---|---|---|---|
atom lifecycle (create+100get+100set) |
9.2µs | 155.1µs | 🟢 16.8× faster |
atom(1) |
2ns | 43ns | 🟢 23.4× faster |
atomFamily: txn update 5,000 existing members |
1.92ms | 7.43ms | 🟢 3.9× faster |
atomFamily(id) |
180ns | 227ns | 🟢 1.3× faster |
atomFamily(id) cache hit |
13ns | 9ns | 🔴 1.5× slower |
atomFamily(string) cache hit |
23ns | 19ns | 🔴 1.2× slower |
createStore |
279ns | 4.2µs | 🟢 15.2× faster |
get 1000 atoms |
9.4µs | 444.2µs | 🟢 47.0× faster |
selector(fn) |
6ns | 45ns | 🟢 8.0× faster |
selectorFamily(id) |
220ns | 329ns | 🟢 1.5× faster |
selectorFamily(string) cache hit |
31ns | 14ns | 🔴 2.2× slower |
set + read 10 selectors |
5.5µs | 23.2µs | 🟢 4.2× faster |
set + read 100 selectorFamily entries |
65.4µs | 169.4µs | 🟢 2.6× faster |
set + read 100 selectors |
52.6µs | 250.5µs | 🟢 4.8× faster |
set + read through 5 chained selectors |
3.8µs | 9.9µs | 🟢 2.6× faster |
set 1000 atoms |
74.7µs | 613.9µs | 🟢 8.2× faster |
set(atom, curr => curr+1) |
100ns | 1.9µs | 🟢 19.2× faster |
set(atom, value) |
110ns | 1.1µs | 🟢 10.4× faster |
set(atom) with 10 subs |
138ns | 1.9µs | 🟢 13.5× faster |
store.get(atom) |
30ns | 231ns | 🟢 7.7× faster |
sub + unsub |
275ns | 1.5µs | 🟢 5.4× faster |
sub+unsub on chain of 100 unsubscribed derived deps |
77.9µs | 94.5µs | 🟢 1.2× faster |
sub+unsub on chain of 50 unsubscribed derived deps |
47.6µs | 51.0µs | 🟢 1.1× faster |
sub+unsub on chain of 500 unsubscribed derived deps |
353.0µs | 453.1µs | 🟢 1.3× faster |
subscribe + unsubscribe 100 shared selector pairs |
522.4µs | 602.3µs | 🟢 1.2× faster |
subscribe + unsubscribe 100 shared selector pairs + fan-in |
488.2µs | 631.3µs | 🟢 1.3× faster |
subscribe + unsubscribe 100 shared selector pairs + fan-in + mounted spine |
1.14ms | 781.0µs | 🔴 1.5× slower |
txn: 10 atoms × 10 selectors, set + read |
80.0µs | 250.9µs | 🟢 3.1× faster |
txn: 10 atoms × 10 selectors, with subs |
77.0µs | 380.6µs | 🟢 4.9× faster |
txn: 10 atoms × 100 selectors, set + read |
729.5µs | 2.43ms | 🟢 3.3× faster |
txn: asymmetric DAG shared sink |
22.3µs | 75.6µs | 🟢 3.4× faster |
txn: cross-atom 1000 selectors, set + read |
894.2µs | 2.50ms | 🟢 2.8× faster |
txn: cross-atom 1000 selectors, with subs |
836.8µs | 13.25ms | 🟢 15.8× faster |
txn: large asymmetric DAG (1000 leaves × 50 chain) |
2.59ms | 9.84ms | 🟢 3.8× faster |
| Operation | valdres | Jotai | |
|---|---|---|---|
atom lifecycle (create+100get+100set) |
27.6µs | 89.4µs | 🟢 3.2× faster |
atom(1) |
19ns | 39ns | 🟢 2.0× faster |
atomFamily: txn update 5,000 existing members |
3.00ms | 4.02ms | 🟢 1.3× faster |
atomFamily(id) |
119ns | 175ns | 🟢 1.5× faster |
atomFamily(id) cache hit |
107ns | 13ns | 🔴 8.2× slower |
atomFamily(string) cache hit |
110ns | 13ns | 🔴 8.7× slower |
createStore |
396ns | 532ns | 🟢 1.3× faster |
get 1000 atoms |
15.8µs | 120.0µs | 🟢 7.6× faster |
selector(fn) |
33ns | 163ns | 🟢 4.9× faster |
selectorFamily(id) |
1.1µs | 577ns | 🔴 1.9× slower |
selectorFamily(string) cache hit |
70ns | 21ns | 🔴 3.3× slower |
set + read 10 selectors |
9.2µs | 13.6µs | 🟢 1.5× faster |
set + read 100 selectorFamily entries |
89.6µs | 102.2µs | 🟢 1.1× faster |
set + read 100 selectors |
89.3µs | 91.0µs | 🟢 1.0× faster |
set + read through 5 chained selectors |
5.1µs | 7.7µs | 🟢 1.5× faster |
set 1000 atoms |
89.6µs | 249.5µs | 🟢 2.8× faster |
set(atom, curr => curr+1) |
250ns | 942ns | 🟢 3.8× faster |
set(atom, value) |
237ns | 779ns | 🟢 3.3× faster |
set(atom) with 10 subs |
323ns | 1.1µs | 🟢 3.5× faster |
store.get(atom) |
18ns | 114ns | 🟢 6.2× faster |
sub + unsub |
666ns | 702ns | 🟢 1.1× faster |
sub+unsub on chain of 100 unsubscribed derived deps |
129.4µs | 77.3µs | 🔴 1.7× slower |
sub+unsub on chain of 50 unsubscribed derived deps |
79.5µs | 70.8µs | 🔴 1.1× slower |
sub+unsub on chain of 500 unsubscribed derived deps |
597.3µs | 378.0µs | 🔴 1.6× slower |
subscribe + unsubscribe 100 shared selector pairs |
715.5µs | 333.2µs | 🔴 2.1× slower |
subscribe + unsubscribe 100 shared selector pairs + fan-in |
690.3µs | 360.5µs | 🔴 1.9× slower |
subscribe + unsubscribe 100 shared selector pairs + fan-in + mounted spine |
1.32ms | 370.3µs | 🔴 3.6× slower |
txn: 10 atoms × 10 selectors, set + read |
106.0µs | 109.6µs | 🟢 1.0× faster |
txn: 10 atoms × 10 selectors, with subs |
68.4µs | 161.5µs | 🟢 2.4× faster |
txn: 10 atoms × 100 selectors, set + read |
1.02ms | 958.3µs | 🔴 1.1× slower |
txn: asymmetric DAG shared sink |
20.3µs | 41.8µs | 🟢 2.1× faster |
txn: cross-atom 1000 selectors, set + read |
1.25ms | 1.30ms | 🟢 1.0× faster |
txn: cross-atom 1000 selectors, with subs |
773.5µs | 6.95ms | 🟢 9.0× faster |
txn: large asymmetric DAG (1000 leaves × 50 chain) |
2.43ms | 4.53ms | 🟢 1.9× faster |