SQLite State Schema
LiveStore provides a schema definition language for defining your database tables and mutation definitions. LiveStore automatically migrates your database schema when you change your schema definitions.
Example
import { makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'
import { Filter } from '../types.js'import * as eventsDefs from './events.js'
/** * LiveStore allows you to freely define your app state as SQLite tables (sometimes referred to as "read model") * and even supports arbitary schema changes without the need for manual schema migrations. * * Your app doesn't directly write to those tables, but instead commits events which are then materialized * into state (i.e. SQLite tables). * * LiveStore doesn't sync tables directly, but syncs events instead which are then materialized into the tables * resulting in the same state. * * See docs to learn more: https://next.livestore.dev/docs/reference/state */
const todos = State.SQLite.table({ name: 'todos', columns: { id: State.SQLite.text({ primaryKey: true }), text: State.SQLite.text({ default: '' }), completed: State.SQLite.boolean({ default: false }), deletedAt: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }), },})
// LiveStore aims to provide a unified state management solution (for synced and client-only state),// so to simplify local-only state management, it also offers a client-only document concept// giving you the convenience of `React.useState` with a derived `.set` event and auto-registered materializer.const uiState = State.SQLite.clientDocument({ name: 'uiState', schema: Schema.Struct({ newTodoText: Schema.String, filter: Filter }), default: { // Using the SessionIdSymbol as default id means the UiState will be scoped per client session (i.e. browser tab). id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' }, },})
export const events = { ...eventsDefs, uiStateSet: uiState.set,}
export const tables = { todos, uiState }
const materializers = State.SQLite.materializers(events, { 'v1.TodoCreated': ({ id, text }) => todos.insert({ id, text, completed: false }), 'v1.TodoCompleted': ({ id }) => todos.update({ completed: true }).where({ id }), 'v1.TodoUncompleted': ({ id }) => todos.update({ completed: false }).where({ id }), 'v1.TodoDeleted': ({ id, deletedAt }) => todos.update({ deletedAt }).where({ id }), 'v1.TodoClearedCompleted': ({ deletedAt }) => todos.update({ deletedAt }).where({ completed: true }),})
const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
import { Events, Schema } from '@livestore/livestore'
/** * LiveStore embraces event sourcing, so data changes are defined as events * (sometimes referred to as "write model"). Those events are then synced across clients * and materialize to state (i.e. SQLite tables). * * Once your app is in production, please make sure your event definitions evolve in a backwards compatible way. * It's recommended to version event definitions. Learn more: https://next.livestore.dev/docs/reference/events */
export const todoCreated = Events.synced({ name: 'v1.TodoCreated', schema: Schema.Struct({ id: Schema.String, text: Schema.String }),})
export const todoCompleted = Events.synced({ name: 'v1.TodoCompleted', schema: Schema.Struct({ id: Schema.String }),})
export const todoUncompleted = Events.synced({ name: 'v1.TodoUncompleted', schema: Schema.Struct({ id: Schema.String }),})
export const todoDeleted = Events.synced({ name: 'v1.TodoDeleted', schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),})
export const todoClearedCompleted = Events.synced({ name: 'v1.TodoClearedCompleted', schema: Schema.Struct({ deletedAt: Schema.Date }),})
Schema migrations
Migration strategies:
auto
: Automatically migrate the database to the newest schema and rematerializes the state from the eventlog.manual
: Manually migrate the database to the newest schema.
Client documents
- Meant for convenience
- Client-only
- Goal: Similar ease of use as
React.useState
- When schema changes in a non-backwards compatible way, previous events are dropped and the state is reset
- Don’t use client documents for sensitive data which must not be lost
- Implies
- Table with
id
andvalue
columns ${MyTable}Set
event + materializer (which are auto-registered)
- Table with
Column types
Core SQLite column types
State.SQLite.text
: A text field, returnsstring
.State.SQLite.integer
: An integer field, returnsnumber
.State.SQLite.real
: A real field (floating point number), returnsnumber
.State.SQLite.blob
: A blob field (binary data), returnsUint8Array
.
Higher level column types
State.SQLite.boolean
: An integer field that stores0
forfalse
and1
fortrue
and returns aboolean
.State.SQLite.json
: A text field that stores a stringified JSON object and returns a decoded JSON value.State.SQLite.datetime
: A text field that stores dates as ISO 8601 strings and returns aDate
.State.SQLite.datetimeInteger
: A integer field that stores dates as the number of milliseconds since the epoch and returns aDate
.
Custom column schemas
You can also provide a custom schema for a column which is used to automatically encode and decode the column value.
Example: JSON-encoded struct
import { State, Schema } from '@livestore/livestore'
export const UserMetadata = Schema.Struct({ petName: Schema.String, favoriteColor: Schema.Literal('red', 'blue', 'green'), })
export const userTable = State.SQLite.table({ name: 'user', columns: { id: State.SQLite.text({ primaryKey: true }), name: State.SQLite.text(), metadata: State.SQLite.json({ schema: UserMetadata }), }})
Best Practices
- It’s usually recommend to not distinguish between app state vs app data but rather keep all state in LiveStore.
- This means you’ll rarely use
React.useState
when using LiveStore
- This means you’ll rarely use
- In some cases for “fast changing values” it can make sense to keep a version of a state value outside of LiveStore with a reactive setter for React and a debounced setter for LiveStore to avoid excessive LiveStore mutations. Cases where this can make sense can include:
- Text input / rich text editing
- Scroll position tracking, resize events, move/drag events
- …