@place/data
Data primitives over @place/reactivity. v0.1 ships exactly one helper — collection<T>() — the keyed-CRUD shape that every entity store hand-rolls. The position is deliberate: most app-level "data" problems collapse to a typed array in a State<T[]> plus the existing capability, persistence, and cache primitives. A typed-query layer lands only if a real workload demands one the reactivity primitives can't satisfy.
collection() takes a State<T[]> and returns CRUD helpers over it. The state stays exposed — compose it with persistedState, history(), crossTabAdapter unchanged. Domain logic (id generation, timestamps, validation) lives in the consumer, not the primitive.collection<T>(state, options?)
Builds a keyed-CRUD interface over a State<T[]>. all() and get(key) are reactive reads; add / update / remove are writes. add throws on a duplicate key; update throws if the patch would change the key (use remove + add for a rename) — loud failures over silent corruption.
// collection<T>(state, options?) — keyed CRUD over a State<T[]>.
// The collection operates on a reactive array; the State stays
// exposed so you can compose it with persistedState, history, etc.
import { state } from '@place/reactivity'
import { collection } from '@place/data'
interface Note { id: string; title: string; tags: string[] }
const notes = state<Note[]>([])
const c = collection<Note>(notes, {
// sort comparator for all(); omit for insertion order.
sortBy: (a, b) => a.title.localeCompare(b.title),
})
c.add({ id: 'a', title: 'first', tags: [] })
c.get('a') // → { id: 'a', … } | null (reactive)
c.update('a', { title: 'edit' }) // merge patch into item 'a'
c.remove('a')
c.all() // → readonly Note[], sorted (reactive)
Custom & composite keys
The key extractor defaults to (item) => item.id. Pass id for a differently-named property or a composite key.
// The key defaults to (item) => item.id. Pass `id` for a
// differently-named or composite key.
const users = collection<User>(state<User[]>([]), {
id: (u) => u.uuid,
})
const items = collection<Item>(state<Item[]>([]), {
id: (it) => `${it.org}:${it.slug}`, // composite key
})
Composing with persistence
Because the collection wraps a plain State<T[]>, wrapping that state with persistedState makes every mutation durable — no special integration.
// The underlying State<T[]> stays exposed — wrap it with
// persistedState so the whole collection survives reloads.
import { state } from '@place/reactivity'
import { collection } from '@place/data'
import { persistedState, localStorageAdapter } from '@place/persistence'
const { state: noteState } = persistedState(
localStorageAdapter<Note[]>('notes', []),
)
const notes = collection<Note>(noteState)
// Every add / update / remove now persists automatically — the
// collection mutates the state, persistedState's watch saves it.
See also
- state · watch · derived
- @place/persistence
- @place/search — reactive search over a collection