Patterns & Recipes
Practical patterns for building real applications with Valdres.
Todo list with families
Use atomFamily to model a collection of items, with a separate atom tracking the list of IDs.
import { atom, atomFamily } from "valdres"
const todoAtom = atomFamily()
const todoIdsAtom = atom([])
// Add a todo
function addTodo(store, title) {
const id = crypto.randomUUID()
store.txn(set => {
set(todoAtom(id), { id, title, done: false })
set(todoIdsAtom, ids => [...ids, id])
})
}
// Toggle completion
function toggleTodo(store, id) {
store.set(todoAtom(id), prev => ({ ...prev, done: !prev.done }))
}
In React, render each todo with fine-grained updates — only the changed todo re-renders:
import { useValue, useSetAtom } from "valdres-react"
function TodoList() {
const ids = useValue(todoIdsAtom)
return ids.map(id => <TodoItem key={id} id={id} />)
}
function TodoItem({ id }) {
const todo = useValue(todoAtom(id))
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(store, id)}
/>
{todo.title}
</label>
)
}
Async data fetching
Atoms can be initialized with async functions. They work seamlessly with React Suspense.
import { atom } from "valdres"
import { useValue } from "valdres-react"
import { Suspense } from "react"
const userAtom = atom(async () => {
const res = await fetch("/api/user")
return res.json()
})
function UserProfile() {
const user = useValue(userAtom)
return <div>{user.name}</div>
}
// Wrap in Suspense
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
)
}
Derived collections
Use selectorFamily to derive computed values for each item in a collection.
import { selector, selectorFamily } from "valdres"
const todoAtom = atomFamily()
// Derived: display text for each todo
const todoDisplaySelector = selectorFamily(id => get => {
const todo = get(todoAtom(id))
return todo.done ? `✓ ${todo.title}` : todo.title
})
// Derived: count of completed todos
const completedCountSelector = selector(get => {
const ids = get(todoIdsAtom)
return ids.filter(id => get(todoAtom(id)).done).length
})
Transactions for consistent updates
When multiple atoms need to change together, use transactions. Subscribers only fire once after all updates complete.
import { atom, store } from "valdres"
const balanceA = atom(100)
const balanceB = atom(100)
const myStore = store()
// Without transaction: components see intermediate state
myStore.set(balanceA, prev => prev - 50)
myStore.set(balanceB, prev => prev + 50) // brief moment where $50 is "missing"
// With transaction: atomic update
myStore.txn(set => {
set(balanceA, prev => prev - 50)
set(balanceB, prev => prev + 50)
// Subscribers fire once, after both updates
})
Form state
Use individual atoms for each field instead of one big form object. This gives you fine-grained re-renders — updating one field doesn't re-render the others.
import { atom } from "valdres"
const nameAtom = atom("")
const emailAtom = atom("")
const messageAtom = atom("")
import { useAtom, useValue } from "valdres-react"
function NameField() {
const [name, setName] = useAtom(nameAtom)
return <input value={name} onChange={e => setName(e.target.value)} />
}
function EmailField() {
const [email, setEmail] = useAtom(emailAtom)
return <input value={email} onChange={e => setEmail(e.target.value)} />
}
// Only re-renders when all three change (e.g., for a preview)
function FormPreview() {
const name = useValue(nameAtom)
const email = useValue(emailAtom)
const message = useValue(messageAtom)
return <pre>{JSON.stringify({ name, email, message }, null, 2)}</pre>
}
Subscribing to a family
Unlike other libraries, you can subscribe to an entire atomFamily to get notified when items are added or removed.
import { atomFamily, store } from "valdres"
const userAtom = atomFamily()
const myStore = store()
// Subscribe to the family — callback receives array of all active keys
myStore.sub(userAtom, keys => {
console.log("Active user IDs:", keys)
})
myStore.set(userAtom("user-1"), { name: "Alice" })
// logs: Active user IDs: ["user-1"]
myStore.set(userAtom("user-2"), { name: "Bob" })
// logs: Active user IDs: ["user-1", "user-2"]
State outside of components
Valdres state lives in a store, not in components. You can read and write state from anywhere — event handlers, WebSocket callbacks, service workers, or tests.
import { atom, store } from "valdres"
const myStore = store()
const statusAtom = atom("idle")
// From a WebSocket
ws.addEventListener("message", event => {
const data = JSON.parse(event.data)
myStore.set(statusAtom, data.status)
})
// From a timer
setInterval(() => {
myStore.set(statusAtom, "polling")
}, 5000)
// In a test
myStore.set(statusAtom, "testing")
expect(myStore.get(statusAtom)).toBe("testing")