Rich Text Editing
LiveStore doesn’t yet have built-in rich text primitives. It’s currently recommended to pair LiveStore with a CRDT library (e.g. Yjs, Automerge, or Loro). LiveStore stores a reference (URL/ID) to the CRDT document in its event log. The CRDT library owns the text content and syncs it over its own transport. Both systems sync in parallel. LiveStore handles metadata while the CRDT library handles document bytes.
Example: linking a note to an Automerge document
Section titled “Example: linking a note to an Automerge document”
export const const note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>
note = import State
State.import SQLite
SQLite.function table<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}, Partial<...>>(args: { ...;} & Partial<...>): State.SQLite.TableDef<...> (+2 overloads)
Creates a SQLite table definition from columns or an Effect Schema.
This function supports two main ways to define a table:
- Using explicit column definitions
- Using an Effect Schema (either the
name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columnsconst usersTable = State.SQLite.table({ name: 'users', columns: { id: State.SQLite.text({ primaryKey: true }), name: State.SQLite.text({ nullable: false }), email: State.SQLite.text({ nullable: false }), age: State.SQLite.integer({ nullable: true }), },})
// Using Effect Schema with annotationsimport { Schema } from '@livestore/utils/effect'
const UserSchema = Schema.Struct({ id: Schema.Int.pipe(State.SQLite.withPrimaryKey).pipe(State.SQLite.withAutoIncrement), email: Schema.String.pipe(State.SQLite.withUnique), name: Schema.String, active: Schema.Boolean.pipe(State.SQLite.withDefault(true)), createdAt: Schema.optional(Schema.Date),})
// Option 1: With explicit nameconst usersTable = State.SQLite.table({ name: 'users', schema: UserSchema,})
// Option 2: With name from schema annotation (title or identifier)const AnnotatedUserSchema = UserSchema.annotate({ title: 'users' })const usersTable2 = State.SQLite.table({ schema: AnnotatedUserSchema,})
// Adding indexesconst PostSchema = Schema.Struct({ id: Schema.String.pipe(State.SQLite.withPrimaryKey), title: Schema.String, authorId: Schema.String, createdAt: Schema.Date,}).annotate({ identifier: 'posts' })
const postsTable = State.SQLite.table({ schema: PostSchema, indexes: [ { name: 'idx_posts_author', columns: ['authorId'] }, { name: 'idx_posts_created', columns: ['createdAt'], isUnique: false }, ],})
table({ name: "note"
name: 'note', columns: { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}
columns: { id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;}
id: import State
State.import SQLite
SQLite.const text: <string, string, false, typeof NoDefault, true, false>(args: { schema?: Schema.Codec<string, string, never, never>; default?: typeof NoDefault; nullable?: false; primaryKey?: true; autoIncrement?: false;}) => { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;} (+1 overload)
text({ primaryKey?: true
primaryKey: true }), title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
title: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Schema.Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ default?: ""
default: '' }), crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
crdtDocUrl: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Schema.Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ nullable?: false
nullable: false, default?: ""
default: '' }), },})
export type type Note = { readonly id: string; readonly title: string; readonly crdtDocUrl: string;}
Note = typeof const note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>
note.type Type: Schema.Struct.ReadonlySide<{ readonly id: Schema.Codec<string, string, never, never>; readonly title: Schema.Codec<string, string, never, never>; readonly crdtDocUrl: Schema.Codec<string, string, never, never>;}, "Type">
Typeexport const const tables: { note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;}
tables = { note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>
note }export const const events: { createNote: State.SQLite.EventDef<"v1.CreateNote", Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String; }, "Encoded">>;}
events = { createNote: State.SQLite.EventDef<"v1.CreateNote", Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Encoded">>
createNote: import Events
Events.synced<"v1.CreateNote", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Encoded">>(args: { name: "v1.CreateNote"; schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<...>, never, never>;} & Omit<...>): State.SQLite.EventDef<...>export synced
Creates a synced event definition.
Synced events are sent to the sync backend and distributed to all connected
clients. Use this for collaborative data that should be shared across users
and devices.
Event names should be versioned (e.g., v1.TodoCreated) to support
schema evolution over time.
synced({ name: "v1.CreateNote"
name: 'v1.CreateNote', schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}, "Encoded">, never, never>
schema: import Schema
Schema.function Struct<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}>(fields: { readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}): Schema.Struct<{ readonly noteId: Schema.String; readonly title: Schema.String; readonly crdtDocUrl: Schema.String;}>
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Example (Defining a basic struct)
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, age: Schema.Number, email: Schema.optionalKey(Schema.String)})
// { readonly name: string; readonly age: number; readonly email?: string }type Person = typeof Person.Type
const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 })console.log(alice)// { name: 'Alice', age: 30 }
Struct({ noteId: Schema.String
noteId: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, title: Schema.String
title: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, crdtDocUrl: Schema.String
crdtDocUrl: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, }), }),}
export const const getNoteCrdtRef: (noteId: string) => string | undefined
getNoteCrdtRef = (noteId: string
noteId: string): string | undefined => { return const store: Store<LiveStoreSchema.Any, {}>
store.Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <readonly string[]>(query: Queryable<readonly string[]> | { query: string; bindValues: Bindable; schema?: Schema.Decoder<readonly string[], never>;}, options?: { otelContext?: Context; debugRefreshReason?: RefreshReason;}) => readonly string[]
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 tables: { note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;}
tables.note: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>
note.select: <"crdtDocUrl">(pluckColumn: "crdtDocUrl") => QueryBuilder<readonly string[], State.SQLite.TableDefBase<State.SQLite.SqliteTableDefForInput<"note", { readonly id: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly crdtDocUrl: { columnType: "text"; schema: Schema.Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>>, "select" | ... 2 more ... | "row"> (+1 overload)
Selects and plucks a single column
select('crdtDocUrl').where: (params: Partial<{ readonly id: string | { op: Exclude<QueryBuilder<TResult, TTableDef extends State.SQLite.TableDefBase<any, any>, TWithout extends QueryBuilder.ApiFeature = never>.WhereOps.SingleValue, QueryBuilder.WhereOps.JsonArray>; value: string; } | { op: QueryBuilder.WhereOps.MultiValue; value: readonly string[]; } | undefined; readonly title: string | { op: Exclude<QueryBuilder.WhereOps.SingleValue, QueryBuilder.WhereOps.JsonArray>; value: string; } | { ...; } | undefined; readonly crdtDocUrl: string | ... 2 more ... | undefined;}>) => QueryBuilder<...> (+3 overloads)
where({ id?: string | { op: Exclude<QueryBuilder.WhereOps.SingleValue, QueryBuilder.WhereOps.JsonArray>; value: string;} | { op: QueryBuilder.WhereOps.MultiValue; value: readonly string[];} | undefined
id: noteId: string
noteId }))[0]}
const const Editor: ({ noteCrdtUrl }: { noteCrdtUrl: string;}) => JSX.Element
Editor = ({ noteCrdtUrl: string
noteCrdtUrl }: { noteCrdtUrl: string
noteCrdtUrl: string }) => { const const docUrl: AutomergeUrl
docUrl = noteCrdtUrl: string
noteCrdtUrl as type AutomergeUrl = string & { __documentUrl: true;}
A branded string representing a URL for a document, in the form automerge:<base58check encoded string>; for example, automerge:4NMNnkMhL8jXrdJ9jamS58PAVdXu.
AutomergeUrl const [const doc: Doc<NoteDoc>
doc, const changeDoc: (changeFn: ChangeFn<NoteDoc>, options?: ChangeOptions<NoteDoc> | undefined) => void
changeDoc] = useDocument<NoteDoc>(id: AnyDocumentId, params: UseDocumentSuspendingParams): UseDocumentReturn<NoteDoc> (+1 overload)
useDocument<type NoteDoc = { body: string;}
NoteDoc>(const docUrl: AutomergeUrl
docUrl, { UseDocumentSuspendingParams.suspense: true
suspense: true })
const const handleChange: ChangeEventHandler<HTMLTextAreaElement>
handleChange: type ChangeEventHandler<T = Element> = (event: ChangeEvent<T>) => void
ChangeEventHandler<interface HTMLTextAreaElement
The HTMLTextAreaElement interface provides properties and methods for manipulating the layout and presentation of elements.
HTMLTextAreaElement> = useCallback<(event: ChangeEvent<HTMLTextAreaElement>) => void>(callback: (event: ChangeEvent<HTMLTextAreaElement>) => void, deps: DependencyList): (event: ChangeEvent<HTMLTextAreaElement>) => void
useCallback will return a memoized version of the callback that only changes if one of the inputs
has changed.
useCallback( (event: ChangeEvent<HTMLTextAreaElement>
event) => { const changeDoc: (changeFn: ChangeFn<NoteDoc>, options?: ChangeOptions<NoteDoc> | undefined) => void
changeDoc((draft: NoteDoc
draft: type NoteDoc = { body: string;}
NoteDoc) => { function updateText(doc: Doc<unknown>, path: Prop[], newText: string): void
Update the value of a string
updateText(draft: NoteDoc
draft, ['body'], event: ChangeEvent<HTMLTextAreaElement>
event.ChangeEvent<HTMLTextAreaElement>.target: EventTarget & HTMLTextAreaElement
target.HTMLTextAreaElement.value: string
The value property of the HTMLTextAreaElement interface represents the value of the element as a string, which is an empty string if the widget contains no content. It returns or sets the raw value contained in the control.
value) }) }, [const changeDoc: (changeFn: ChangeFn<NoteDoc>, options?: ChangeOptions<NoteDoc> | undefined) => void
changeDoc], )
return <JSX.IntrinsicElements.textarea: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>
textarea TextareaHTMLAttributes<HTMLTextAreaElement>.value?: string | number | readonly string[] | undefined
value={const doc: Doc<NoteDoc>
doc?.body: string
body ?? ''} TextareaHTMLAttributes<HTMLTextAreaElement>.onChange?: ChangeEventHandler<HTMLTextAreaElement> | undefined
onChange={const handleChange: ChangeEventHandler<HTMLTextAreaElement>
handleChange} />}
export const const RichTextNoteEditor: ({ noteId }: { noteId: string;}) => JSX.Element | null
RichTextNoteEditor = ({ noteId: string
noteId }: { noteId: string
noteId: string }) => { const const noteCrdtUrl: string | undefined
noteCrdtUrl = const getNoteCrdtRef: (noteId: string) => string | undefined
getNoteCrdtRef(noteId: string
noteId) if (const noteCrdtUrl: string | undefined
noteCrdtUrl == null) return null
return <const Editor: ({ noteCrdtUrl }: { noteCrdtUrl: string;}) => JSX.Element
Editor noteCrdtUrl: string
noteCrdtUrl={const noteCrdtUrl: string
noteCrdtUrl} />}