# Reactivity system

LiveStore has a high-performance, fine-grained reactivity system built in which is similar to Signals (e.g. in [SolidJS](https://docs.solidjs.com/concepts/signals)).

## Defining reactive state

LiveStore provides 3 types of reactive state:
- Reactive SQL queries on top of SQLite state (`queryDb()`)
- Reactive state values (`signal()`)
- Reactive computed values (`computed()`)

Reactive state variables end on a `$` by convention (e.g. `todos$`). The `label` option is optional but can be used to identify the reactive state variable in the devtools.

### Reactive SQL queries


## `reference/reactivity-system/query-db.ts`

```ts filename="reference/reactivity-system/query-db.ts"
/** biome-ignore-all lint/correctness/noUnusedVariables: docs snippet exposes intermediate streams */
// ---cut---
import { queryDb, signal } from '@livestore/livestore'

import { tables } from '../framework-integrations/react/schema.ts'

const uiState$ = signal({ showCompleted: false }, { label: 'uiState$' })

const todos$ = queryDb(tables.todos.orderBy('createdAt', 'desc'), { label: 'todos$' })

{
  const todos$ = queryDb(
    (get) => {
      const { showCompleted } = get(uiState$)
      return tables.todos.where(showCompleted === true ? { completed: true } : {})
    },
    { label: 'todos$' },
  )
}
```

### `reference/framework-integrations/react/schema.ts`

```ts filename="reference/framework-integrations/react/schema.ts"
import { defineMaterializer, Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean({ default: false }),
      createdAt: State.SQLite.datetime(),
    },
  }),
  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' } },
  }),
} as const

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String, createdAt: Schema.Date }),
  }),
} as const

const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, createdAt }) =>
    tables.todos.insert({ id, text, completed: false, createdAt }),
  ),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

### Signals

Signals are reactive state values that can be set and get. This can be useful for state that is not materialized from events into SQLite tables.


## `reference/reactivity-system/signals.ts`

```ts filename="reference/reactivity-system/signals.ts"
import { type Store, signal } from '@livestore/livestore'

import type { schema } from '../framework-integrations/react/schema.ts'

declare const store: Store<typeof schema>

const now$ = signal(Date.now(), { label: 'now$' })

setInterval(() => {
  store.setSignal(now$, Date.now())
}, 1000)

const num$ = signal(0, { label: 'num$' })
const increment = () => store.setSignal(num$, (prev) => prev + 1)

increment()
increment()

console.log(store.query(num$))
```

### `reference/framework-integrations/react/schema.ts`

```ts filename="reference/framework-integrations/react/schema.ts"
import { defineMaterializer, Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean({ default: false }),
      createdAt: State.SQLite.datetime(),
    },
  }),
  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' } },
  }),
} as const

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String, createdAt: Schema.Date }),
  }),
} as const

const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, createdAt }) =>
    tables.todos.insert({ id, text, completed: false, createdAt }),
  ),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

### Computed values


## `reference/reactivity-system/computed.ts`

```ts filename="reference/reactivity-system/computed.ts"
/** biome-ignore-all lint/correctness/noUnusedVariables: docs snippet highlights derived signal */
// ---cut---
import { computed, signal } from '@livestore/livestore'

const num$ = signal(0, { label: 'num$' })
const duplicated$ = computed((get) => get(num$) * 2, { label: 'duplicated$' })
```

## Accessing reactive state

Reactive state is always bound to a `Store` instance. You can access the current value of reactive state the following ways:

### Using the `Store` instance


## `reference/reactivity-system/store-access.ts`

```ts filename="reference/reactivity-system/store-access.ts"
import { queryDb, type Store } from '@livestore/livestore'

import { type schema, tables } from '../framework-integrations/react/schema.ts'

declare const store: Store<typeof schema>

const count$ = queryDb(tables.todos.count(), { label: 'count$' })
const count = store.query(count$)
console.log(count)

const unsubscribe = store.subscribe(count$, (value) => {
  console.log(value)
})

unsubscribe()
```

### `reference/framework-integrations/react/schema.ts`

```ts filename="reference/framework-integrations/react/schema.ts"
import { defineMaterializer, Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean({ default: false }),
      createdAt: State.SQLite.datetime(),
    },
  }),
  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' } },
  }),
} as const

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String, createdAt: Schema.Date }),
  }),
} as const

const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, createdAt }) =>
    tables.todos.insert({ id, text, completed: false, createdAt }),
  ),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

### Via framework integrations

#### React


## `reference/reactivity-system/react-component.tsx`

```tsx filename="reference/reactivity-system/react-component.tsx"
import type { FC } from 'react'

import { queryDb } from '@livestore/livestore'

import { tables } from '../framework-integrations/react/schema.ts'
import { useAppStore } from '../framework-integrations/react/store.ts'

const todos$ = queryDb(tables.todos.orderBy('createdAt', 'desc'), { label: 'todos' })

export const TodoList: FC = () => {
  const store = useAppStore()
  const todos = store.useQuery(todos$)

  return <div>{todos.length} items</div>
}
```

### `reference/framework-integrations/react/schema.ts`

```ts filename="reference/framework-integrations/react/schema.ts"
import { defineMaterializer, Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean({ default: false }),
      createdAt: State.SQLite.datetime(),
    },
  }),
  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' } },
  }),
} as const

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String, createdAt: Schema.Date }),
  }),
} as const

const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, createdAt }) =>
    tables.todos.insert({ id, text, completed: false, createdAt }),
  ),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

### `reference/framework-integrations/react/store.ts`

```ts filename="reference/framework-integrations/react/store.ts"
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'

import { makeInMemoryAdapter } from '@livestore/adapter-web'
import { useStore } from '@livestore/react'

import { schema } from './schema.ts'

const adapter = makeInMemoryAdapter()

export const useAppStore = () =>
  useStore({
    storeId: 'app-root',
    schema,
    adapter,
    batchUpdates,
  })
```

#### Solid


## `reference/reactivity-system/solid-component.tsx`

```tsx filename="reference/reactivity-system/solid-component.tsx"
import type { LiveQueryDef, Store } from '@livestore/livestore'

declare const store: Store & { useQuery: <T>(query: LiveQueryDef<T>) => () => T }
declare const state$: LiveQueryDef<number>

export const MyComponent = () => {
  const value = store.useQuery(state$)

  return <div>{value()}</div>
}
```

### Reacting to changing variables passed to queries

If your query depends on a variable passed in by the component, use the deps array to react to changes in this variable.


## `reference/reactivity-system/deps-query.tsx`

```tsx filename="reference/reactivity-system/deps-query.tsx"
import type { FC } from 'react'

import { queryDb } from '@livestore/livestore'

import { tables } from '../framework-integrations/react/schema.ts'
import { useAppStore } from '../framework-integrations/react/store.ts'

export const todos$ = ({ showCompleted }: { showCompleted: boolean }) =>
  queryDb(
    () => {
      return tables.todos.where(showCompleted === true ? { completed: true } : {})
    },
    {
      label: 'todos$',
      deps: [showCompleted === true ? 'true' : 'false'],
    },
  )

export const MyComponent: FC<{ showCompleted: boolean }> = ({ showCompleted }) => {
  const store = useAppStore()
  const todos = store.useQuery(todos$({ showCompleted })) as ReadonlyArray<{
    id: string
    text: string
    completed: boolean
  }>

  return <div>{todos.length} Done</div>
}
```

### `reference/framework-integrations/react/schema.ts`

```ts filename="reference/framework-integrations/react/schema.ts"
import { defineMaterializer, Events, makeSchema, Schema, SessionIdSymbol, State } from '@livestore/livestore'

export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text(),
      completed: State.SQLite.boolean({ default: false }),
      createdAt: State.SQLite.datetime(),
    },
  }),
  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' } },
  }),
} as const

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String, createdAt: Schema.Date }),
  }),
} as const

const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, createdAt }) =>
    tables.todos.insert({ id, text, completed: false, createdAt }),
  ),
})

const state = State.SQLite.makeState({ tables, materializers })

export const schema = makeSchema({ events, state })
```

### `reference/framework-integrations/react/store.ts`

```ts filename="reference/framework-integrations/react/store.ts"
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'

import { makeInMemoryAdapter } from '@livestore/adapter-web'
import { useStore } from '@livestore/react'

import { schema } from './schema.ts'

const adapter = makeInMemoryAdapter()

export const useAppStore = () =>
  useStore({
    storeId: 'app-root',
    schema,
    adapter,
    batchUpdates,
  })
```

## Further reading

- [Riffle](https://riffle.systems/essays/prelude/): Building data-centric apps with a reactive relational database
- [Adapton](http://adapton.org/) / [miniAdapton](https://arxiv.org/pdf/1609.05337)

## Related technologies

- [Signia](https://signia.tldraw.dev/): Signia is a minimal, fast, and scalable signals library for TypeScript developed by TLDraw.