Skip to content

Reactivity system

LiveStore has a high-performance, fine-grained reactivity system built in which is similar to Signals (e.g. in SolidJS).

LiveStore provides 3 types of reactive state:

  • Reactive SQL queries on top of SQLite state (queryDb())
  • Reactive state values (signal())
  • Reactive computed values (computed())

Reactive state variables end on a $ by convention (e.g. todos$). The label option is optional but can be used to identify the reactive state variable in the devtools.

import {
const queryDb: {
<TResultSchema, TResult = TResultSchema>(queryInput: QueryInputRaw<TResultSchema, ReadonlyArray<any>> | QueryBuilder<TResultSchema, any, any>, options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
<TResultSchema, TResult = TResultSchema>(queryInput: ((get: GetAtomResult) => QueryInputRaw<TResultSchema, ReadonlyArray<any>>) | ((get: GetAtomResult) => QueryBuilder<TResultSchema, any, any>), options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
}

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
,
const signal: <T>(defaultValue: T, options?: {
label?: string;
}) => SignalDef<T>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
} from '@livestore/livestore'
import {
import tables
tables
} from '../framework-integrations/react/schema.ts'
const
const uiState$: SignalDef<{
showCompleted: boolean;
}>
uiState$
=
signal<{
showCompleted: boolean;
}>(defaultValue: {
showCompleted: boolean;
}, options?: {
label?: string;
}): SignalDef<{
showCompleted: boolean;
}>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
({
showCompleted: boolean
showCompleted
: false }, {
label?: string
label
: 'uiState$' })
const
const todos$: LiveQueryDef<unknown, "def">
todos$
=
queryDb<unknown, unknown>(queryInput: QueryInputRaw<unknown, readonly any[]> | QueryBuilder<unknown, any, any>, options?: {
map?: (rows: unknown) => unknown;
label?: string;
deps?: DepKey;
} | undefined): LiveQueryDef<unknown, "def"> (+1 overload)

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
(
import tables
tables
.
any
todos
.
any
orderBy
('createdAt', 'desc'), {
label?: string

Used for debugging / devtools

label
: 'todos$' })
{
const
const todos$: LiveQueryDef<unknown, "def">
todos$
=
queryDb<unknown, unknown>(queryInput: ((get: GetAtomResult) => QueryInputRaw<unknown, readonly any[]>) | ((get: GetAtomResult) => QueryBuilder<unknown, any, any>), options?: {
map?: (rows: unknown) => unknown;
label?: string;
deps?: DepKey;
} | undefined): LiveQueryDef<unknown, "def"> (+1 overload)

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
(
(
get: GetAtomResult
get
) => {
const {
const showCompleted: boolean
showCompleted
} =
get: <{
showCompleted: boolean;
}>(atom: SignalDef<{
showCompleted: boolean;
}> | Atom<{
showCompleted: boolean;
}, any, RefreshReason> | LiveQueryDef<{
showCompleted: boolean;
}, "def"> | LiveQuery<{
showCompleted: boolean;
}> | ISignal<{
showCompleted: boolean;
}>, otelContext?: Context, debugRefreshReason?: RefreshReason) => {
showCompleted: boolean;
}
get
(
const uiState$: SignalDef<{
showCompleted: boolean;
}>
uiState$
)
return
import tables
tables
.
any
todos
.
any
where
(
const showCompleted: boolean
showCompleted
=== true ? {
completed: boolean
completed
: true } : {})
},
{
label?: string

Used for debugging / devtools

label
: 'todos$' },
)
}

Signals are reactive state values that can be set and get. This can be useful for state that is not materialized from events into SQLite tables.

import { type
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
,
const signal: <T>(defaultValue: T, options?: {
label?: string;
}) => SignalDef<T>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
} from '@livestore/livestore'
import type {
import schema
schema
} from '../framework-integrations/react/schema.ts'
declare const
const store: Store<any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
<typeof
import schema
schema
>
const
const now$: SignalDef<number>
now$
=
signal<number>(defaultValue: number, options?: {
label?: string;
}): SignalDef<number>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
(
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
(), {
label?: string
label
: 'now$' })
function setInterval<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+4 overloads)
setInterval
(() => {
const store: Store<any, {}>
store
.
Store<any, {}>.setSignal: <number>(signalDef: SignalDef<number>, value: number | ((prev: number) => number)) => void

Set the value of a signal

@example

const count$ = signal(0, { label: 'count$' })
store.setSignal(count$, 2)

@example

const count$ = signal(0, { label: 'count$' })
store.setSignal(count$, (prev) => prev + 1)

setSignal
(
const now$: SignalDef<number>
now$
,
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
())
}, 1000)
const
const num$: SignalDef<number>
num$
=
signal<number>(defaultValue: number, options?: {
label?: string;
}): SignalDef<number>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
(0, {
label?: string
label
: 'num$' })
const
const increment: () => void
increment
= () =>
const store: Store<any, {}>
store
.
Store<any, {}>.setSignal: <number>(signalDef: SignalDef<number>, value: number | ((prev: number) => number)) => void

Set the value of a signal

@example

const count$ = signal(0, { label: 'count$' })
store.setSignal(count$, 2)

@example

const count$ = signal(0, { label: 'count$' })
store.setSignal(count$, (prev) => prev + 1)

setSignal
(
const num$: SignalDef<number>
num$
, (
prev: number
prev
) =>
prev: number
prev
+ 1)
const increment: () => void
increment
()
const increment: () => void
increment
()
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const store: Store<any, {}>
store
.
Store<any, {}>.query: <number>(query: Queryable<number> | {
query: string;
bindValues: Bindable;
schema?: Decoder<number, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => number

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
const num$: SignalDef<number>
num$
))
import {
const computed: <TResult>(fn: (get: GetAtomResult) => TResult, options?: {
label?: string;
deps?: DepKey;
}) => LiveQueryDef<TResult>

Creates a derived query that computes a value from other queries or signals.

Computed queries are memoized—they only re-evaluate when their dependencies change, and if the new result equals the previous result, downstream dependents won't re-run. Use them for expensive calculations, aggregations, or transformations.

The get function inside computed establishes reactive dependencies automatically. When any dependency updates, the computed re-evaluates.

@example

// Derive a count from a database query
const todos$ = queryDb(tables.todos.all())
const todoCount$ = computed((get) => get(todos$).length, { label: 'todoCount' })
// Use in a component
const count = store.query(todoCount$) // 5

@example

// Combine multiple queries into derived stats
const stats$ = computed((get) => {
const todos = get(todos$)
const completed = todos.filter((t) => t.completed).length
return {
total: todos.length,
completed,
remaining: todos.length - completed,
percentComplete: todos.length > 0 ? (completed / todos.length) * 100 : 0,
}
}, { label: 'todoStats' })

@example

// Chain computed queries
const hasCompletedTodos$ = computed(
(get) => get(stats$).completed > 0,
{ label: 'hasCompletedTodos' }
)

@paramfn - Pure function that computes the result. Use get() to read dependencies.

@paramoptions.label - Human-readable label for debugging and devtools

@paramoptions.deps - Explicit dependency keys (required on Expo/React Native where fn.toString() returns [native code])

@returnsA query definition usable with store.query(), store.subscribe(), and as a dependency in other queries

computed
,
const signal: <T>(defaultValue: T, options?: {
label?: string;
}) => SignalDef<T>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
} from '@livestore/livestore'
const
const num$: SignalDef<number>
num$
=
signal<number>(defaultValue: number, options?: {
label?: string;
}): SignalDef<number>

Creates a reactive signal for ephemeral, local-only state that isn't persisted to the database.

Signals are useful for UI state that needs to trigger query re-evaluation but shouldn't be synced across clients or stored permanently—such as search filters, selected items, or temporary form values.

Unlike database-backed state (via events), signals:

  • Are not persisted or synced
  • Exist only for the lifetime of the Store
  • Can hold any value type (primitives, objects, functions)

@example

// Create a signal for search text
const searchText$ = signal('', { label: 'searchText' })
// Create a query that depends on the signal
const filteredTodos$ = queryDb(
(get) => tables.todos.where({ text: { $like: `%${get(searchText$)}%` } }),
{ deps: [searchText$] }
)
// Update the signal (triggers query re-evaluation)
store.setSignal(searchText$, 'buy')
// Read the current value
const results = store.query(filteredTodos$)

@example

// Counter with functional updates
const count$ = signal(0, { label: 'count' })
store.setSignal(count$, (prev) => prev + 1)

@paramdefaultValue - Initial value of the signal

@paramoptions.label - Human-readable label for debugging and devtools

@returnsA signal definition that can be used with store.query(), store.setSignal(), and as a dependency in other queries

signal
(0, {
label?: string
label
: 'num$' })
const
const duplicated$: LiveQueryDef<number, "def">
duplicated$
=
computed<number>(fn: (get: GetAtomResult) => number, options?: {
label?: string;
deps?: DepKey;
}): LiveQueryDef<number, "def">

Creates a derived query that computes a value from other queries or signals.

Computed queries are memoized—they only re-evaluate when their dependencies change, and if the new result equals the previous result, downstream dependents won't re-run. Use them for expensive calculations, aggregations, or transformations.

The get function inside computed establishes reactive dependencies automatically. When any dependency updates, the computed re-evaluates.

@example

// Derive a count from a database query
const todos$ = queryDb(tables.todos.all())
const todoCount$ = computed((get) => get(todos$).length, { label: 'todoCount' })
// Use in a component
const count = store.query(todoCount$) // 5

@example

// Combine multiple queries into derived stats
const stats$ = computed((get) => {
const todos = get(todos$)
const completed = todos.filter((t) => t.completed).length
return {
total: todos.length,
completed,
remaining: todos.length - completed,
percentComplete: todos.length > 0 ? (completed / todos.length) * 100 : 0,
}
}, { label: 'todoStats' })

@example

// Chain computed queries
const hasCompletedTodos$ = computed(
(get) => get(stats$).completed > 0,
{ label: 'hasCompletedTodos' }
)

@paramfn - Pure function that computes the result. Use get() to read dependencies.

@paramoptions.label - Human-readable label for debugging and devtools

@paramoptions.deps - Explicit dependency keys (required on Expo/React Native where fn.toString() returns [native code])

@returnsA query definition usable with store.query(), store.subscribe(), and as a dependency in other queries

computed
((
get: GetAtomResult
get
) =>
get: <number>(atom: SignalDef<number> | Atom<number, any, RefreshReason> | LiveQueryDef<number, "def"> | LiveQuery<number> | ISignal<number>, otelContext?: Context, debugRefreshReason?: RefreshReason) => number
get
(
const num$: SignalDef<number>
num$
) * 2, {
label?: string
label
: 'duplicated$' })

Reactive state is always bound to a Store instance. You can access the current value of reactive state the following ways:

import {
const queryDb: {
<TResultSchema, TResult = TResultSchema>(queryInput: QueryInputRaw<TResultSchema, ReadonlyArray<any>> | QueryBuilder<TResultSchema, any, any>, options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
<TResultSchema, TResult = TResultSchema>(queryInput: ((get: GetAtomResult) => QueryInputRaw<TResultSchema, ReadonlyArray<any>>) | ((get: GetAtomResult) => QueryBuilder<TResultSchema, any, any>), options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
}

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
, type
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import { type
import schema
schema
,
import tables
tables
} from '../framework-integrations/react/schema.ts'
declare const
const store: Store<any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
<typeof
import schema
schema
>
const
const count$: LiveQueryDef<unknown, "def">
count$
=
queryDb<unknown, unknown>(queryInput: QueryInputRaw<unknown, readonly any[]> | QueryBuilder<unknown, any, any>, options?: {
map?: (rows: unknown) => unknown;
label?: string;
deps?: DepKey;
} | undefined): LiveQueryDef<unknown, "def"> (+1 overload)

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
(
import tables
tables
.
any
todos
.
any
count
(), {
label?: string

Used for debugging / devtools

label
: 'count$' })
const
const count: unknown
count
=
const store: Store<any, {}>
store
.
Store<any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
const count$: LiveQueryDef<unknown, "def">
count$
)
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const count: unknown
count
)
const
const unsubscribe: Unsubscribe
unsubscribe
=
const store: Store<any, {}>
store
.
Store<any, {}>.subscribe: <unknown>(query: Queryable<unknown>, onUpdate: (value: unknown) => void, options?: SubscribeOptions<unknown> | undefined) => Unsubscribe (+1 overload)
subscribe
(
const count$: LiveQueryDef<unknown, "def">
count$
, (
value: unknown
value
) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
value: unknown
value
)
})
const unsubscribe: () => void
unsubscribe
()
import type {
type FC<P = {}> = FunctionComponent<P>

Represents the type of a function component. Can optionally receive a type argument that represents the props the component receives.

@seehttps://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components React TypeScript Cheatsheet

@aliasfor FunctionComponent

@example

// With props:
type Props = { name: string }
const MyComponent: FC<Props> = (props) => {
return <div>{props.name}</div>
}

@example

// Without props:
const MyComponentWithoutProps: FC = () => {
return <div>MyComponentWithoutProps</div>
}

FC
} from 'react'
import {
const queryDb: {
<TResultSchema, TResult = TResultSchema>(queryInput: QueryInputRaw<TResultSchema, ReadonlyArray<any>> | QueryBuilder<TResultSchema, any, any>, options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
<TResultSchema, TResult = TResultSchema>(queryInput: ((get: GetAtomResult) => QueryInputRaw<TResultSchema, ReadonlyArray<any>>) | ((get: GetAtomResult) => QueryBuilder<TResultSchema, any, any>), options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
}

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
} from '@livestore/livestore'
import {
import tables
tables
} from '../framework-integrations/react/schema.ts'
import {
import useAppStore
useAppStore
} from '../framework-integrations/react/store.ts'
const
const todos$: LiveQueryDef<unknown, "def">
todos$
=
queryDb<unknown, unknown>(queryInput: QueryInputRaw<unknown, readonly any[]> | QueryBuilder<unknown, any, any>, options?: {
map?: (rows: unknown) => unknown;
label?: string;
deps?: DepKey;
} | undefined): LiveQueryDef<unknown, "def"> (+1 overload)

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
(
import tables
tables
.
any
todos
.
any
orderBy
('createdAt', 'desc'), {
label?: string

Used for debugging / devtools

label
: 'todos' })
export const
const TodoList: FC
TodoList
:
type FC<P = {}> = FunctionComponent<P>

Represents the type of a function component. Can optionally receive a type argument that represents the props the component receives.

@seehttps://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components React TypeScript Cheatsheet

@aliasfor FunctionComponent

@example

// With props:
type Props = { name: string }
const MyComponent: FC<Props> = (props) => {
return <div>{props.name}</div>
}

@example

// Without props:
const MyComponentWithoutProps: FC = () => {
return <div>MyComponentWithoutProps</div>
}

FC
= () => {
const
const store: any
store
=
import useAppStore
useAppStore
()
const
const todos: any
todos
=
const store: any
store
.
any
useQuery
(
const todos$: LiveQueryDef<unknown, "def">
todos$
)
return <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{
const todos: any
todos
.
any
length
} items</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
}
import type {
interface LiveQueryDef<TResult, TTag extends string = "def">

A query definition representing a blueprint for a reactive query.

Query definitions are created by

queryDb

,

computed

, and

signal

. They're lightweight and can be defined at module scope. The actual query instance (which holds state) is created lazily when you use the definition with a Store.

Multiple uses of the same definition share a single instance via reference counting.

LiveQueryDef
,
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
declare const
const store: Store<LiveStoreSchema.Any, {}> & {
useQuery: <T>(query: LiveQueryDef<T>) => () => T;
}
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
& {
useQuery: <T>(query: LiveQueryDef<T>) => () => T
useQuery
: <
function (type parameter) T in <T>(query: LiveQueryDef<T>): () => T
T
>(
query: LiveQueryDef<T, "def">
query
:
interface LiveQueryDef<TResult, TTag extends string = "def">

A query definition representing a blueprint for a reactive query.

Query definitions are created by

queryDb

,

computed

, and

signal

. They're lightweight and can be defined at module scope. The actual query instance (which holds state) is created lazily when you use the definition with a Store.

Multiple uses of the same definition share a single instance via reference counting.

LiveQueryDef
<
function (type parameter) T in <T>(query: LiveQueryDef<T>): () => T
T
>) => () =>
function (type parameter) T in <T>(query: LiveQueryDef<T>): () => T
T
}
declare const
const state$: LiveQueryDef<number, "def">
state$
:
interface LiveQueryDef<TResult, TTag extends string = "def">

A query definition representing a blueprint for a reactive query.

Query definitions are created by

queryDb

,

computed

, and

signal

. They're lightweight and can be defined at module scope. The actual query instance (which holds state) is created lazily when you use the definition with a Store.

Multiple uses of the same definition share a single instance via reference counting.

LiveQueryDef
<number>
export const
const MyComponent: () => JSX.Element
MyComponent
= () => {
const
const value: () => number
value
=
const store: Store<LiveStoreSchema.Any, {}> & {
useQuery: <T>(query: LiveQueryDef<T>) => () => T;
}
store
.
useQuery: <number>(query: LiveQueryDef<number, "def">) => () => number
useQuery
(
const state$: LiveQueryDef<number, "def">
state$
)
return <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{
const value: () => number
value
()}</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
}

Reacting to changing variables passed to queries

Section titled “Reacting to changing variables passed to queries”

If your query depends on a variable passed in by the component, use the deps array to react to changes in this variable.

import type {
type FC<P = {}> = FunctionComponent<P>

Represents the type of a function component. Can optionally receive a type argument that represents the props the component receives.

@seehttps://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components React TypeScript Cheatsheet

@aliasfor FunctionComponent

@example

// With props:
type Props = { name: string }
const MyComponent: FC<Props> = (props) => {
return <div>{props.name}</div>
}

@example

// Without props:
const MyComponentWithoutProps: FC = () => {
return <div>MyComponentWithoutProps</div>
}

FC
} from 'react'
import {
const queryDb: {
<TResultSchema, TResult = TResultSchema>(queryInput: QueryInputRaw<TResultSchema, ReadonlyArray<any>> | QueryBuilder<TResultSchema, any, any>, options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
<TResultSchema, TResult = TResultSchema>(queryInput: ((get: GetAtomResult) => QueryInputRaw<TResultSchema, ReadonlyArray<any>>) | ((get: GetAtomResult) => QueryBuilder<TResultSchema, any, any>), options?: {
map?: (rows: TResultSchema) => TResult;
label?: string;
deps?: DepKey;
}): LiveQueryDef<TResult>;
}

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
} from '@livestore/livestore'
import {
import tables
tables
} from '../framework-integrations/react/schema.ts'
import {
import useAppStore
useAppStore
} from '../framework-integrations/react/store.ts'
export const
const todos$: ({ showCompleted }: {
showCompleted: boolean;
}) => LiveQueryDef<unknown, "def">
todos$
= ({
showCompleted: boolean
showCompleted
}: {
showCompleted: boolean
showCompleted
: boolean }) =>
queryDb<unknown, unknown>(queryInput: ((get: GetAtomResult) => QueryInputRaw<unknown, readonly any[]>) | ((get: GetAtomResult) => QueryBuilder<unknown, any, any>), options?: {
map?: (rows: unknown) => unknown;
label?: string;
deps?: DepKey;
} | undefined): LiveQueryDef<unknown, "def"> (+1 overload)

NOTE queryDb is only supposed to read data. Don't use it to insert/update/delete data but use events instead.

When using contextual data when constructing the query, please make sure to include it in the deps option.

@example

const todos$ = queryDb(tables.todos.where({ complete: true }))

@example

// Group-by raw SQL query
const colorCounts$ = queryDb({
query: sql`SELECT color, COUNT(*) as count FROM todos WHERE complete = ? GROUP BY color`,
schema: Schema.Array(Schema.Struct({
color: Schema.String,
count: Schema.Number,
})),
bindValues: [1],
})

@example

// Using contextual data when constructing the query
const makeFilteredQuery = (filter: string) =>
queryDb(tables.todos.where({ title: { op: 'like', value: filter } }), { deps: [filter] })
const filteredTodos$ = makeFilteredQuery('buy coffee')

queryDb
(
() => {
return
import tables
tables
.
any
todos
.
any
where
(
showCompleted: boolean
showCompleted
=== true ? {
completed: boolean
completed
: true } : {})
},
{
label?: string

Used for debugging / devtools

label
: 'todos$',
deps?: DepKey
deps
: [
showCompleted: boolean
showCompleted
=== true ? 'true' : 'false'],
},
)
export const
const MyComponent: FC<{
showCompleted: boolean;
}>
MyComponent
:
type FC<P = {}> = FunctionComponent<P>

Represents the type of a function component. Can optionally receive a type argument that represents the props the component receives.

@seehttps://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components React TypeScript Cheatsheet

@aliasfor FunctionComponent

@example

// With props:
type Props = { name: string }
const MyComponent: FC<Props> = (props) => {
return <div>{props.name}</div>
}

@example

// Without props:
const MyComponentWithoutProps: FC = () => {
return <div>MyComponentWithoutProps</div>
}

FC
<{
showCompleted: boolean
showCompleted
: boolean }> = ({
showCompleted: boolean
showCompleted
}) => {
const
const store: any
store
=
import useAppStore
useAppStore
()
const
const todos: readonly {
id: string;
text: string;
completed: boolean;
}[]
todos
=
const store: any
store
.
any
useQuery
(
const todos$: ({ showCompleted }: {
showCompleted: boolean;
}) => LiveQueryDef<unknown, "def">
todos$
({
showCompleted: boolean
showCompleted
})) as
interface ReadonlyArray<T>
ReadonlyArray
<{
id: string
id
: string
text: string
text
: string
completed: boolean
completed
: boolean
}>
return <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{
const todos: readonly {
id: string;
text: string;
completed: boolean;
}[]
todos
.
ReadonlyArray<T>.length: number

Gets the length of the array. This is a number one higher than the highest element defined in an array.

length
} Done</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
}
  • Signia: Signia is a minimal, fast, and scalable signals library for TypeScript developed by TLDraw.