Works Everywhere
Unlike most state management libraries, Valdres doesn't depend on any framework. The core valdres package is pure JavaScript — it works in Node.js, Deno, Bun, web workers, service workers, or any JavaScript runtime.
This is a fundamental design choice, not an afterthought. Your state logic lives in valdres, and framework bindings (valdres-react, valdres-vue, etc.) are thin adapters that subscribe to it.
Vanilla JavaScript
import { atom, selector, store } from "valdres"
const myStore = store()
const countAtom = atom(0)
const doubleSelector = selector(get => get(countAtom) * 2)
myStore.get(countAtom) // 0
myStore.get(doubleSelector) // 0
myStore.set(countAtom, 5)
myStore.get(doubleSelector) // 10
// Subscribe to changes
const unsub = myStore.sub(countAtom, () => {
console.log("count changed:", myStore.get(countAtom))
})
myStore.set(countAtom, 10)
// logs: "count changed: 10"
unsub() // stop listening
Node.js / server-side
Valdres works on the server without any special configuration. This is useful for server-side rendering, API routes, or background jobs that share state definitions with the frontend.
// shared/state.ts — shared between client and server
import { atom, atomFamily } from "valdres"
export const sessionAtom = atom(null)
export const cacheAtom = atomFamily()
// server.ts
import { store } from "valdres"
import { sessionAtom, cacheAtom } from "./shared/state"
const serverStore = store()
// Pre-populate state on the server
serverStore.set(sessionAtom, { userId: "abc", role: "admin" })
serverStore.set(cacheAtom("config"), await loadConfig())
// Pass state to the client via serialization
const snapshot = {
session: serverStore.get(sessionAtom),
config: serverStore.get(cacheAtom("config")),
}
// Release global-atom registrations held for this request.
serverStore.dispose()
For real request handlers, put dispose() in a finally block so thrown
renders and aborted requests are cleaned up too.
Web Workers
Since Valdres has no DOM dependency, it runs natively in web workers. Keep heavy computation off the main thread while sharing state definitions.
// worker.ts
import { atom, store } from "valdres"
const progressAtom = atom(0)
const workerStore = store()
self.onmessage = async (event) => {
const items = event.data
for (let i = 0; i < items.length; i++) {
await processItem(items[i])
workerStore.set(progressAtom, (i + 1) / items.length)
self.postMessage({ progress: workerStore.get(progressAtom) })
}
}
Testing
Testing with Valdres is straightforward — create a store, set state, assert. No providers, no mocking, no framework overhead.
import { atom, selector, store } from "valdres"
import { expect, test } from "bun:test"
const countAtom = atom(0)
const doubleSelector = selector(get => get(countAtom) * 2)
test("selector derives from atom", () => {
const testStore = store()
testStore.set(countAtom, 5)
expect(testStore.get(doubleSelector)).toBe(10)
})
test("transactions are atomic", () => {
const testStore = store()
const a = atom(100)
const b = atom(100)
const total = selector(get => get(a) + get(b))
testStore.txn(({ set }) => {
set(a, 50)
set(b, 150)
})
expect(testStore.get(total)).toBe(200)
})
Sharing state across frameworks
Because state lives in the store — not in components — you can share the same atoms across different frameworks in the same app. This is what powers the demo on our homepage.
// shared.ts — import this from React, Vue, Svelte, or anything else
import { atom, store } from "valdres"
export const appStore = store()
export const countAtom = atom(0)
export const themeAtom = atom("dark")
Each framework adapter just subscribes to the same store:
// In React
import { useValue } from "valdres-react"
import { countAtom } from "./shared"
const count = useValue(countAtom)
<!-- In Vue -->
<script setup>
import { useValue } from "valdres-vue"
import { countAtom } from "./shared"
const count = useValue(countAtom)
</script>
Why this matters
Most state libraries are tightly coupled to a specific framework's rendering model. This means:
- You can't test state logic without mounting components
- You can't share state between micro-frontends using different frameworks
- You can't use state in workers, CLI tools, or server code
- Migration between frameworks requires rewriting state management
Valdres avoids all of these problems by keeping state management framework-agnostic at its core.