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
Section titled “Example”import { Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'
// You can model your state as SQLite tables (https://docs.livestore.dev/reference/state/sqlite-schema)export const tables = { 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 }), }, }), // Client documents can be used for local-only state (e.g. form inputs) uiState: State.SQLite.clientDocument({ name: 'uiState', schema: Schema.Struct({ newTodoText: Schema.String, filter: Schema.Literal('all', 'active', 'completed') }), default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } }, }),}
// Events describe data changes (https://docs.livestore.dev/reference/events)export const events = { todoCreated: Events.synced({ name: 'v1.TodoCreated', schema: Schema.Struct({ id: Schema.String, text: Schema.String }), }), todoCompleted: Events.synced({ name: 'v1.TodoCompleted', schema: Schema.Struct({ id: Schema.String }), }), todoUncompleted: Events.synced({ name: 'v1.TodoUncompleted', schema: Schema.Struct({ id: Schema.String }), }), todoDeleted: Events.synced({ name: 'v1.TodoDeleted', schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }), }), todoClearedCompleted: Events.synced({ name: 'v1.TodoClearedCompleted', schema: Schema.Struct({ deletedAt: Schema.Date }), }), uiStateSet: tables.uiState.set,}
// Materializers are used to map events to state (https://docs.livestore.dev/reference/state/materializers)const materializers = State.SQLite.materializers(events, { 'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text, completed: false }), 'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }), 'v1.TodoUncompleted': ({ id }) => tables.todos.update({ completed: false }).where({ id }), 'v1.TodoDeleted': ({ id, deletedAt }) => tables.todos.update({ deletedAt }).where({ id }), 'v1.TodoClearedCompleted': ({ deletedAt }) => tables.todos.update({ deletedAt }).where({ completed: true }),})
const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
Schema migrations
Section titled “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
Section titled “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
Section titled “Column types”Core SQLite column types
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
- …