Store
See source codeTable of contents
A reactive store that manages collections of typed records.
The Store is the central container for your application's data, providing:
- Reactive state management with automatic updates
- Type-safe record operations
- History tracking and change notifications
- Schema validation and migrations
- Side effects and business logic hooks
- Efficient querying and indexing
class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {}
Example
// Create a store with schema
const schema = StoreSchema.create({
book: Book,
author: Author,
})
const store = new Store({
schema,
props: {},
})
// Add records
const book = Book.create({ title: 'The Lathe of Heaven', author: 'Le Guin' })
store.put([book])
// Listen to changes
store.listen((entry) => {
console.log('Changes:', entry.changes)
})
Constructor
Creates a new Store instance.
Example
const store = new Store({
schema: StoreSchema.create({ book: Book }),
props: { appName: 'MyLibrary' },
initialData: savedData,
})
Parameters
Name | Description |
---|---|
|
Configuration object for the store |
Properties
history
An atom containing the store's history.
readonly history: Atom<number, RecordsDiff<R>>
id
The unique identifier of the store instance.
readonly id: string
props
Custom properties associated with this store instance.
readonly props: Props
query
Reactive queries and indexes for efficiently accessing store data. Provides methods for filtering, indexing, and subscribing to subsets of records.
readonly query: StoreQueries<R>
Example
// Create an index by a property
const booksByAuthor = store.query.index('book', 'author')
// Get records matching criteria
const inStockBooks = store.query.records('book', () => ({
inStock: { eq: true },
}))
schema
The schema that defines the structure and validation rules for records in this store.
readonly schema: StoreSchema<R, Props>
scopedTypes
A mapping of record scopes to the set of record type names that belong to each scope. Used to filter records by their persistence and synchronization behavior.
readonly scopedTypes: {
readonly [K in RecordScope]: ReadonlySet<R['typeName']>
}
sideEffects
Side effects manager that handles lifecycle events for record operations. Allows registration of callbacks for create, update, delete, and validation events.
readonly sideEffects: StoreSideEffects<R>
Example
store.sideEffects.registerAfterCreateHandler('book', (book) => {
console.log('Book created:', book.title)
})
Methods
allRecords( )
Get an array of all records in the store.
allRecords(): R[]
Example
const allRecords = store.allRecords()
const books = allRecords.filter((r) => r.typeName === 'book')
applyDiff( )
applyDiff(
diff: RecordsDiff<R>,
{
runCallbacks,
ignoreEphemeralKeys,
}?: {
ignoreEphemeralKeys?: boolean
runCallbacks?: boolean
}
): void
Parameters
Name | Description |
---|---|
|
|
|
|
Returns
void
clear( )
Remove all records from the store.
clear(): void
Example
store.clear()
console.log(store.allRecords().length) // 0
createCache( )
Create a cache based on values in the store. Pass in a function that takes and ID and a signal for the underlying record. Return a signal (usually a computed) for the cached value. For simple derivations, use Store.createComputedCache. This function is useful if you need more precise control over intermediate values.
createCache<Result, Record extends R = R>(
create: (id: IdOf<Record>, recordSignal: Signal<R>) => Signal<Result>
): {
get: (id: IdOf<Record>) => Result | undefined
}
Parameters
Returns
{
get: (id: IdOf<Record>) => Result | undefined
}
createComputedCache( )
Create a computed cache.
createComputedCache<Result, Record extends R = R>(
name: string,
derive: (record: Record) => Result | undefined,
opts?: CreateComputedCacheOpts<Result, Record>
): ComputedCache<Result, Record>
Parameters
Name | Description |
---|---|
|
The name of the derivation cache. |
|
A function used to derive the value of the cache. |
|
Options for the computed cache. |
Returns
ComputedCache<Result, Record>
dispose( )
dispose(): void
extractingChanges( )
Run fn
and return a RecordsDiff of the changes that occurred as a result.
extractingChanges(fn: () => void): RecordsDiff<R>
Parameters
Name | Description |
---|---|
|
|
Returns
RecordsDiff<R>
filterChangesByScope( )
Filters out non-document changes from a diff. Returns null if there are no changes left.
filterChangesByScope(
change: RecordsDiff<R>,
scope: RecordScope
): {
added: { [K in IdOf<R>]: R }
removed: { [K in IdOf<R>]: R }
updated: { [K_1 in IdOf<R>]: [from: R, to: R] }
} | null
Parameters
Name | Description |
---|---|
|
the records diff |
| the records scope |
Returns
{
added: { [K in IdOf<R>]: R }
removed: { [K in IdOf<R>]: R }
updated: { [K_1 in IdOf<R>]: [from: R, to: R] }
} | null
get( )
Get a record by its ID. This creates a reactive subscription to the record.
get<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined
Example
const book = store.get(bookId)
if (book) {
console.log(book.title)
}
Parameters
Name | Description |
---|---|
|
The ID of the record to get |
Returns
RecordFromId<K> | undefined
The record if it exists, undefined otherwise
getStoreSnapshot( )
Get a serialized snapshot of the store and its schema. This includes both the data and schema information needed for proper migration.
getStoreSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>
Example
const snapshot = store.getStoreSnapshot()
localStorage.setItem('myApp', JSON.stringify(snapshot))
// Later...
const saved = JSON.parse(localStorage.getItem('myApp'))
store.loadStoreSnapshot(saved)
Parameters
Name | Description |
---|---|
|
The scope of records to serialize. Defaults to 'document' |
Returns
A snapshot containing both store data and schema information
has( )
Check whether a record with the given ID exists in the store.
has<K extends IdOf<R>>(id: K): boolean
Example
if (store.has(bookId)) {
console.log('Book exists!')
}
Parameters
Name | Description |
---|---|
|
The ID of the record to check |
Returns
boolean
True if the record exists, false otherwise
listen( )
Add a listener that will be called when the store changes. Returns a function to remove the listener.
listen(
onHistory: StoreListener<R>,
filters?: Partial<StoreListenerFilters>
): () => void
Example
const removeListener = store.listen((entry) => {
console.log('Changes:', entry.changes)
console.log('Source:', entry.source)
})
// Listen only to user changes to document records
const removeDocumentListener = store.listen(
(entry) => console.log('Document changed:', entry),
{ source: 'user', scope: 'document' }
)
// Later, remove the listener
removeListener()
Parameters
Name | Description |
---|---|
| The listener function to call when changes occur |
|
Optional filters to control when the listener is called |
Returns
() => void
A function that removes the listener when called
loadStoreSnapshot( )
Load a serialized snapshot into the store, replacing all current data. The snapshot will be automatically migrated to the current schema version if needed.
loadStoreSnapshot(snapshot: StoreSnapshot<R>): void
Example
const snapshot = JSON.parse(localStorage.getItem('myApp'))
store.loadStoreSnapshot(snapshot)
Parameters
Name | Description |
---|---|
| The snapshot to load |
Returns
void
mergeRemoteChanges( )
Merge changes from a remote source. Changes made within the provided function will be marked with source 'remote' instead of 'user'.
mergeRemoteChanges(fn: () => void): void
Example
// Changes from sync/collaboration
store.mergeRemoteChanges(() => {
store.put(remoteRecords)
store.remove(deletedIds)
})
Parameters
Name | Description |
---|---|
|
A function that applies the remote changes |
Returns
void
migrateSnapshot( )
Migrate a serialized snapshot to the current schema version. This applies any necessary migrations to bring old data up to date.
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>
Example
const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
const migratedSnapshot = store.migrateSnapshot(oldSnapshot)
Parameters
Name | Description |
---|---|
| The snapshot to migrate |
Returns
The migrated snapshot with current schema version
put( )
Add or update records in the store. If a record with the same ID already exists, it will be updated. Otherwise, a new record will be created.
put(records: R[], phaseOverride?: 'initialize'): void
Example
// Add new records
const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })
store.put([book])
// Update existing record
store.put([{ ...book, title: 'The Lathe of Heaven' }])
Parameters
Name | Description |
---|---|
|
The records to add or update |
|
Override the validation phase (used internally) |
Returns
void
remove( )
Remove records from the store by their IDs.
remove(ids: IdOf<R>[]): void
Example
// Remove a single record
store.remove([book.id])
// Remove multiple records
store.remove([book1.id, book2.id, book3.id])
Parameters
Name | Description |
---|---|
|
The IDs of the records to remove |
Returns
void
serialize( )
Serialize the store's records to a plain JavaScript object. Only includes records matching the specified scope.
serialize(scope?: 'all' | RecordScope): SerializedStore<R>
Example
// Serialize only document records (default)
const documentData = store.serialize('document')
// Serialize all records
const allData = store.serialize('all')
Parameters
Name | Description |
---|---|
|
The scope of records to serialize. Defaults to 'document' |
Returns
The serialized store data
unsafeGetWithoutCapture( )
Get a record by its ID without creating a reactive subscription. Use this when you need to access a record but don't want reactive updates.
unsafeGetWithoutCapture<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined
Example
// Won't trigger reactive updates when this record changes
const book = store.unsafeGetWithoutCapture(bookId)
Parameters
Name | Description |
---|---|
|
The ID of the record to get |
Returns
RecordFromId<K> | undefined
The record if it exists, undefined otherwise
update( )
Update a single record using an updater function. To update multiple records at once,
use the update
method of the TypedStore
class.
update<K extends IdOf<R>>(
id: K,
updater: (record: RecordFromId<K>) => RecordFromId<K>
): void
Example
store.update(book.id, (book) => ({
...book,
title: 'Updated Title',
}))
Parameters
Name | Description |
---|---|
|
The ID of the record to update |
|
A function that receives the current record and returns the updated record |
Returns
void
validate( )
validate(
phase: 'createRecord' | 'initialize' | 'tests' | 'updateRecord'
): void
Parameters
Name | Description |
---|---|
|
|
Returns
void