Client

Live queries

Components that follow the data, with useLiveQuery and useLiveIncrementalQuery.

PGlite's live extension re-runs a query whenever the tables it reads change. The module wraps it in composables that return reactive refs and unsubscribe when the component unmounts. Add live to clientExtensions first:

app/pglite.config.ts
import { live } from '@electric-sql/pglite/live'

export default definePGliteClientConfig({
  clientExtensions: { live },
})

useLiveQuery

const { rows, fields } = useLiveQuery<Todo>('SELECT * FROM todos ORDER BY id')

rows and fields are read-only refs, undefined until the first result arrives. Add, toggle or delete below, then open this page in a second tab: the worker is shared, so both lists follow every change.

app/components/Todos.client.vue
<script setup lang="ts">
interface Todo {
  id: number
  title: string
  done: boolean
}

const { rows: todos } = useLiveQuery<Todo>('SELECT * FROM todos ORDER BY id')

async function add(title: string) {
  const pg = await usePGlite()
  await pg.query('INSERT INTO todos (title) VALUES ($1)', [title])
}

async function toggle(todo: Todo) {
  const pg = await usePGlite()
  await pg.query('UPDATE todos SET done = NOT done WHERE id = $1', [todo.id])
}

async function remove(todo: Todo) {
  const pg = await usePGlite()
  await pg.query('DELETE FROM todos WHERE id = $1', [todo.id])
}
</script>

The writes do not touch todos: the live query notices the change and updates the ref.

Parameters

The query accepts a string, a ref or a getter; each parameter a plain value, a ref (a computed included) or a getter. When one of them changes, the composable re-subscribes:

const search = ref('')
const pattern = computed(() => `%${search.value}%`)

const { rows } = useLiveQuery<Todo>('SELECT * FROM todos WHERE title ILIKE $1 ORDER BY id', [
  pattern,
])

Tagged template

useLiveQuery.sql builds the query and its parameters from a template literal, so interpolated values are sent as parameters, not concatenated:

const minimum = 2
const { rows } = useLiveQuery.sql<Planet>`SELECT * FROM planets WHERE moons >= ${minimum}`

The template form is evaluated once: use the function form with refs for values that change.

useLiveIncrementalQuery

For large result sets, the incremental variant diffs the result by a key column instead of replacing every row:

const { rows } = useLiveIncrementalQuery<Todo>('SELECT * FROM todos ORDER BY id', [], 'id')

The third argument is the column that uniquely identifies a row; like the query, it also accepts a ref or a getter.

Where they can run

The composables subscribe in the browser only and throw during server-side rendering. Call them in the setup of a .client.vue component, or of a component rendered inside <ClientOnly>. They need live in clientExtensions; without it they fail with a message saying so.