0.0.57 • Published 9 months ago

@livestore/livestore v0.0.57

Weekly downloads
-
License
-
Repository
-
Last release
9 months ago

Discord

Local setup

# Install deps and build libs
pnpm install
pnpm build

# Run the example app
cd examples/todomvc
pnpm dev

Caveats

  • Only supports recent browser versions (Safari 17+, ...)
  • Doesn't yet run in Next.js (easiest to use with Vite right now)

Features

  • Synchronous, transactional reads and writes
  • Otel tracing built-in

Concepts

LiveStore provides a highly structured data model for your React components. It helps you clearly reason about data dependencies, leverage the relational model and the power of SQLite, and persist UI state.

Reads

To define the data used by a component, you use the useQuery hook. There are 2 parts to defining the data:

  • local state: This is the equivalent of React.useState, but in a relational style. Each component instance gets a row in a database table to store local state. You define the schema for the table that stores this component state. In your component, you can read/write local state.
  • reactive queries: Often it's not enough to just read/write local state, you also need to query data in the global database (eg, table of todos, or table of music tracks). To do this, you can define reactive SQL or GraphQL queries. The query strings can be dynamic and depend on local state or other queries. You can also .pipe the results of any SQL or GraphQL query to do further downstream transformations.

Let's see an example. This doesn't have any local state, just queries.

We have a todos app which has a global table called app, which always has one row. It has a column called filter which has the value active, completed, or all. We want to use this value to query for only the todos which should be visible with that filter. Here's the code:

import { querySQL, sql } from '@livestore/livestore'
import { useQuery } from '@livestore/livestore/react'

const filterClause$ = querySQL<AppState[]>(`select * from app;`)
  .pipe(([appState]) => (appState.filter === 'all' ? '' : `where completed = ${appState.filter === 'completed'}`))

const visibleTodos$ = querySQL<Todo[]>((get) => sql`select * from todos ${get(filterClause$)}`)


export const MyApp: React.FC = () => {
  const visibleTodos = useQuery(visibleTodos$)

  return (
    // ...
  )
}

Writes

Writes happen through mutations: structured mutations on the LiveStore datastore. Think closer to Redux-style mutations at the domain level, rather than low-level SQL writes. This makes it clearer what's going on in the code, and enables other things like sync / undo in the future.

Write mutations can be accessed via the useLiveStoreActions hook. This is global and not component-scoped. (If you want to do a write that references some local state, you can just pass it in to the mutation arguements.)

const { store } = useStore()

// We record an event that specifies marking complete or incomplete,
// The reason is that this better captures the user's intention
// when the event gets synced across multiple devices--
// If another user toggled concurrently, we shouldn't toggle it back
const toggleTodo = (todo: Todo) =>
  store.mutate(todo.completed ? mutations.uncompleteTodo({ id: todo.id }) : mutations.completeTodo({ id: todo.id }))

Defining dependencies

LiveStore tracks which tables are read by each query and written by each mutation, in order to determine which queries need to be re-run in response to each write.

In the future we want to do this more automatically via analysis of queries, but currently this write/read table tracking is done manually. It's very important to correctly annotate write and reads with table names, otherwise reactive updates won't work correctly.

Here's how writes and reads are annotated.

Write mutations: annotate the SQL statement in the mutation definition, like this:

export const completeTodo = defineMutation(
  'completeTodo',
  Schema.Struct({ id: Schema.String }),
  sql`UPDATE todos SET completed = true WHERE id = $id`,
)

GraphQL: annotate the query in the resolver, like this:

spotifyAlbum = (albumId: string) => {
  this.queriedTables.add('album_images').add('albums')

  const albums = this.db.select<AlbumSrc[]>(sql`
      select id, name,
      (
        select image_url
        from ${tableNames.album_images}
        where album_images.album_id = albums.id
        order by height desc -- use the big image for this view
        limit 1
      ) as image_url
      from albums
      where id = '${albumId}'
    `)

  return albums[0] ?? null
}

SQL: manual table annotation is not supported yet on queries, todo soon.

0.0.58-dev.8

9 months ago

0.0.58-dev.7

9 months ago

0.0.58-dev.9

9 months ago

0.0.58-dev.0

10 months ago

0.0.58-dev.2

10 months ago

0.0.58-dev.1

10 months ago

0.0.58-dev.4

10 months ago

0.0.58-dev.3

10 months ago

0.0.58-dev.6

10 months ago

0.0.58-dev.5

10 months ago

0.0.57

10 months ago

0.0.57-dev.8

10 months ago

0.0.57-dev.7

10 months ago

0.0.57-dev.6

10 months ago

0.0.57-dev.5

10 months ago

0.0.57-dev.4

10 months ago

0.0.57-dev.3

11 months ago

0.0.57-dev.2

11 months ago

0.0.57-dev.1

11 months ago

0.0.57-dev.0

11 months ago

0.0.54-dev.24

1 year ago

0.0.54-dev.25

1 year ago

0.0.54-dev.26

1 year ago

0.0.54-dev.21

1 year ago

0.0.54-dev.22

1 year ago

0.0.54-dev.27

12 months ago

0.0.54-dev.28

12 months ago

0.0.54-dev.29

12 months ago

0.0.54-dev.13

1 year ago

0.0.54-dev.15

1 year ago

0.0.56-dev.3

11 months ago

0.0.54-dev.16

1 year ago

0.0.54-dev.17

1 year ago

0.0.56-dev.1

11 months ago

0.0.54-dev.18

1 year ago

0.0.56-dev.2

11 months ago

0.0.54-dev.19

1 year ago

0.0.56-dev.0

11 months ago

0.0.51

1 year ago

0.0.52

1 year ago

0.0.55-dev.0

12 months ago

0.0.53

1 year ago

0.0.48-dev.6

1 year ago

0.0.55-dev.1

12 months ago

0.0.54

12 months ago

0.0.55

12 months ago

0.0.56

11 months ago

0.0.55-dev.2

12 months ago

0.0.54-dev.30

12 months ago

0.0.55-dev.3

12 months ago

0.0.54-dev.31

12 months ago

0.0.54-dev.32

12 months ago

0.0.54-dev.33

12 months ago

0.0.50

1 year ago

0.0.48

1 year ago

0.0.48-dev.0

1 year ago

0.0.49

1 year ago

0.0.48-dev.1

1 year ago

0.0.48-dev.4

1 year ago

0.0.48-dev.5

1 year ago

0.0.48-dev.2

1 year ago

0.0.48-dev.3

1 year ago

0.0.53-dev.0

1 year ago

0.0.53-dev.1

1 year ago

0.0.53-dev.2

1 year ago

0.0.54-dev.5

1 year ago

0.0.54-dev.3

1 year ago

0.0.54-dev.4

1 year ago

0.0.54-dev.1

1 year ago

0.0.54-dev.2

1 year ago

0.0.54-dev.0

1 year ago

0.0.51-dev.0

1 year ago

0.0.47

1 year ago

0.0.47-dev.0

1 year ago

0.0.46

1 year ago

0.0.46-dev.4

1 year ago

0.0.46-dev.2

1 year ago

0.0.46-dev.1

1 year ago

0.0.46-dev.0

1 year ago

0.0.44

1 year ago

0.0.45

1 year ago

0.0.43

1 year ago

0.0.42

1 year ago

0.0.42-dev.0

1 year ago

0.0.41

1 year ago

0.0.41-dev.2

1 year ago

0.0.41-dev.1

1 year ago

0.0.41-dev.0

1 year ago

0.0.40

1 year ago

0.0.39

1 year ago

0.0.39-dev.3

1 year ago

0.0.39-dev.2

1 year ago

0.0.37

2 years ago

0.0.38

2 years ago

0.0.36

2 years ago

0.0.35

2 years ago

0.0.34

2 years ago

0.0.31

2 years ago

0.0.32

2 years ago

0.0.30

2 years ago

0.0.28

2 years ago

0.0.29

2 years ago

0.0.27

2 years ago

0.0.25

2 years ago

0.0.24

2 years ago

0.0.23

2 years ago

0.0.22

2 years ago

0.0.21

2 years ago

0.0.14

2 years ago

0.0.19

2 years ago

0.0.16

2 years ago

0.0.15

2 years ago

0.0.13

2 years ago

0.0.12

2 years ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.0

2 years ago