Getting started with LiveStore + Vue
Prerequisites
Section titled “Prerequisites”- Recommended: Bun 1.2 or higher
- Node.js 23.0.0 or higher
About Vue integration
Section titled “About Vue integration”Vue integration is still in beta and being incubated as a separate repository. Please direct any issues or contributions to Vue LiveStore
Option A: Quick start
Section titled “Option A: Quick start”For a quick start, we recommend referencing the playground folder in the Vue LiveStore repository.
Option B: Existing project setup
Section titled “Option B: Existing project setup”-
Install dependencies
It’s strongly recommended to use
bun
orpnpm
for the simplest and most reliable dependency setup (see note on package management for more details).Terminal window bun install @livestore/livestore @livestore/wa-sqlite@1.0.5-dev.2 @livestore/adapter-web @livestore/utils @livestore/peer-deps @livestore/devtools-vite slashv/vue-livestoreTerminal window pnpm add @livestore/livestore @livestore/wa-sqlite@1.0.5-dev.2 @livestore/adapter-web @livestore/utils @livestore/peer-deps @livestore/devtools-vite slashv/vue-livestoreTerminal window npm install @livestore/livestore @livestore/wa-sqlite@1.0.5-dev.2 @livestore/adapter-web @livestore/utils @livestore/peer-deps @livestore/devtools-vite slashv/vue-livestore -
Update Vite config
Add the following code to your
vite.config.js
file:import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'import vueDevTools from 'vite-plugin-vue-devtools'export default defineConfig({plugins: [vue(),vueDevTools(),livestoreDevtoolsPlugin({ schemaPath: './src/livestore/schema.ts' }),],worker: { format: 'es' },})
Define Your Schema
Section titled “Define Your Schema”Create a file named schema.ts
inside the src/livestore
folder. This file defines your LiveStore schema consisting of your app’s event definitions (describing how data changes), derived state (i.e. SQLite tables), and materializers (how state is derived from events).
Here’s an example schema:
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 })
Create the LiveStore Worker
Section titled “Create the LiveStore Worker”Create a file named livestore.worker.ts
inside the src/livestore
folder. This file will contain the LiveStore web worker. When importing this file, make sure to add the ?worker
extension to the import path to ensure that Vite treats it as a worker file.
import { makeWorker } from '@livestore/adapter-web/worker'import { makeCfSync } from '@livestore/sync-cf'
import { schema } from './livestore/schema.js'
makeWorker({ schema, sync: { backend: makeCfSync({ url: import.meta.env.VITE_LIVESTORE_SYNC_URL }), initialSyncOptions: { _tag: 'Blocking', timeout: 5000 }, },})
Add the LiveStore Provider
Section titled “Add the LiveStore Provider”To make the LiveStore available throughout your app, wrap your app’s root component with the LiveStoreProvider
component from vue-livestore
. This provider manages your app’s data store, loading, and error states.
Here’s an example:
<script setup lang="ts">import { makePersistedAdapter } from '@livestore/adapter-web'import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'import LiveStoreWorker from './livestore/livestore.worker?worker'import { schema } from './livestore/schema'import { LiveStoreProvider } from 'vue-livestore'import ToDos from './components/to-dos.vue'
const adapter = makePersistedAdapter({ storage: { type: 'opfs' }, worker: LiveStoreWorker, sharedWorker: LiveStoreSharedWorker,})
const storeOptions = { schema, adapter, storeId: 'test_store',}</script>
<template> <LiveStoreProvider :options="storeOptions"> <template #loading> <div>Loading LiveStore...</div> </template> <ToDos /> </LiveStoreProvider></template>
Commit events
Section titled “Commit events”After wrapping your app with the LiveStoreProvider
, you can use the useStore
hook from any component to commit events.
Here’s an example:
<script setup lang="ts">import { ref } from 'vue'import { events } from '../livestore/schema'const { store } = useStore()
const newTodoText = ref('')
// Eventsconst createTodo = () => { store.commit(events.todoCreated({ id: crypto.randomUUID(), text: newTodoText.value })) newTodoText.value = ''}</script>
<template> <div> <input v-model="newTodoText" /> <button @click="createTodo">Create</button> </div></template>
Queries
Section titled “Queries”To retrieve data from the database, first define a query using queryDb
from @livestore/livestore
. Then, execute the query with the useQuery
hook from @livestore/react
.
Consider abstracting queries into a separate file to keep your code organized, though you can also define them directly within components if preferred.
Here’s an example:
<script setup lang="ts">import { queryDb } from '@livestore/livestore'import { useQuery } from 'vue-livestore'import { events, tables } from '../livestore/schema'
const visibleTodos$ = queryDb( () => tables.todos.where({ deletedAt: null }), { label: 'visibleTodos' },)
const todos = useQuery(visibleTodos$)</script>
<template> <div> <ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} </li> </ul> </div></template>