atomFamily
atomFamily<K, T>(defaultFactory?: (key: K) => T, options?: AtomFamilyOptions): AtomFamily<K, T>valdres Create a keyed collection of atoms
Creates a family of atoms keyed by a parameter. Useful for collections of entities like users, todos, or items where each instance needs its own piece of state.
Usage
import { atomFamily } from "valdres"
// Simple family with no default
const todoAtom = atomFamily()
store.set(todoAtom("abc-123"), { title: "Buy milk", done: false })
store.get(todoAtom("abc-123")) // { title: "Buy milk", done: false }
With a default value factory
Pass a function that receives the key and returns the default value:
const userAtom = atomFamily(id => ({
id,
name: "",
email: "",
}))
// The default is generated when first accessed
store.get(userAtom("user-1")) // { id: "user-1", name: "", email: "" }
Global families
Use globalAtomFamily to share each member's value
across stores. options.name is required — a global family is addressed by
it, so omitting it is a type error and remains a runtime error for untyped
JavaScript.
import { globalAtomFamily } from "valdres"
const featureFlagAtom = globalAtomFamily((key: string) => false, {
name: "app/feature-flags",
})
Re-defining a family under the same name returns the existing family instead of creating a second one. Selectors do not have a global counterpart.
Key identity
Family calls are keyed by a deterministic structural encoding. Primitive types,
argument count, Arrays, plain Objects, Dates, Maps, and Sets remain distinct;
object property order and Map/Set insertion order do not affect identity.
BigInt is supported too.
Values without deterministic structural semantics—such as Symbols, functions,
Promises, class instances, accessor properties, and cyclic structures—throw a
TypeError. Use keyOf to derive a supported identity for those arguments, or
to intentionally group multiple arguments:
type Entity = { id: string; self?: Entity }
const entityAtom = atomFamily<Entity, [Entity]>(entity => entity, {
keyOf: entity => entity.id,
})
keyOf receives the same argument tuple as the family factory. Its result is
run through the same canonical codec.
dehydrate emits family args raw (schemas encode values, not
keys) and hydrate re-derives each member with family(...args) from the
parsed payload. A Date, Map, Set, BigInt, NaN or undefined
argument does not survive that round-trip, so the value would land on a
phantom member while the real one keeps its default. Dev builds throw from
dehydrate, naming the family and the argument path. Key transferred
families by strings and numbers (user(id), day(date.toISOString())) —
keyOf does not help here, since the raw args are what crosses the wire.For the same reason, a named family's keyOf must derive its key from the
arguments' JSON data (entity => entity.id). JSON carries no property
descriptors, no frozen/sealed flag, and no object identity — two references
to one object arrive as two objects — so a keyOf reading any of those
returns a different key after hydration. dehydrate cannot detect this and
does not try.
The legacy release(...args) method is deprecated and has no effect. Family
members leave the weak identity cache automatically once no caller or store can
reach them. Manual eviction could otherwise create two live member objects for
the same arguments and split their values across stores.
Subscribing to a family
store.sub(todoAtom, id => {
console.log("Changed todo ID:", id)
})
In React
import { useValue } from "valdres-react"
function UserProfile({ userId }) {
const user = useValue(userAtom(userId))
return <div>{user.name}</div>
}
See also
- atom — create a single atom
- globalAtomFamily — share each member's value across every store
- selectorFamily — derive state from a family of atoms
- index — reactively filter family members by a term