@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/QueryCache.js +1 -1
- package/dist/QueryCache.js.map +1 -1
- package/dist/SqliteDbWrapper.d.ts +5 -5
- package/dist/SqliteDbWrapper.d.ts.map +1 -1
- package/dist/SqliteDbWrapper.js +8 -8
- package/dist/SqliteDbWrapper.js.map +1 -1
- package/dist/SqliteDbWrapper.test.js +2 -2
- package/dist/SqliteDbWrapper.test.js.map +1 -1
- package/dist/effect/LiveStore.d.ts +130 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +185 -6
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/LiveStore.test.d.ts +2 -0
- package/dist/effect/LiveStore.test.d.ts.map +1 -0
- package/dist/effect/LiveStore.test.js +42 -0
- package/dist/effect/LiveStore.test.js.map +1 -0
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/live-queries/base-class.d.ts +3 -3
- package/dist/live-queries/base-class.d.ts.map +1 -1
- package/dist/live-queries/base-class.js +2 -2
- package/dist/live-queries/base-class.js.map +1 -1
- package/dist/live-queries/client-document-get-query.d.ts +1 -1
- package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
- package/dist/live-queries/client-document-get-query.js +1 -1
- package/dist/live-queries/client-document-get-query.js.map +1 -1
- package/dist/live-queries/computed.d.ts.map +1 -1
- package/dist/live-queries/computed.js +2 -2
- package/dist/live-queries/computed.js.map +1 -1
- package/dist/live-queries/db-query.js +14 -14
- package/dist/live-queries/db-query.js.map +1 -1
- package/dist/live-queries/db-query.test.js +2 -2
- package/dist/live-queries/db-query.test.js.map +1 -1
- package/dist/live-queries/signal.test.js +2 -2
- package/dist/live-queries/signal.test.js.map +1 -1
- package/dist/mod.d.ts +2 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/reactive.d.ts +9 -9
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +9 -26
- package/dist/reactive.js.map +1 -1
- package/dist/reactive.test.js +2 -2
- package/dist/reactive.test.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +215 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +267 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +381 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +56 -6
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +32 -7
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +16 -3
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-eventstream.test.js +2 -2
- package/dist/store/store-eventstream.test.js.map +1 -1
- package/dist/store/store-types.d.ts +59 -9
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store-types.test.js +1 -1
- package/dist/store/store-types.test.js.map +1 -1
- package/dist/store/store.d.ts +102 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +148 -47
- package/dist/store/store.js.map +1 -1
- package/dist/utils/dev.js.map +1 -1
- package/dist/utils/stack-info.js +2 -2
- package/dist/utils/stack-info.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +1 -1
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/dist/utils/tests/fixture.js.map +1 -1
- package/dist/utils/tests/otel.d.ts.map +1 -1
- package/dist/utils/tests/otel.js +5 -5
- package/dist/utils/tests/otel.js.map +1 -1
- package/package.json +59 -18
- package/src/QueryCache.ts +1 -1
- package/src/SqliteDbWrapper.test.ts +4 -2
- package/src/SqliteDbWrapper.ts +12 -11
- package/src/ambient.d.ts +0 -7
- package/src/effect/LiveStore.test.ts +61 -0
- package/src/effect/LiveStore.ts +381 -8
- package/src/effect/mod.ts +13 -1
- package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
- package/src/live-queries/base-class.ts +7 -6
- package/src/live-queries/client-document-get-query.ts +4 -2
- package/src/live-queries/computed.ts +3 -2
- package/src/live-queries/db-query.test.ts +3 -2
- package/src/live-queries/db-query.ts +15 -15
- package/src/live-queries/signal.test.ts +3 -2
- package/src/mod.ts +2 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +540 -0
- package/src/store/StoreRegistry.ts +418 -0
- package/src/store/create-store.ts +76 -15
- package/src/store/devtools.ts +20 -6
- package/src/store/store-eventstream.test.ts +4 -2
- package/src/store/store-types.test.ts +3 -1
- package/src/store/store-types.ts +64 -13
- package/src/store/store.ts +197 -60
- package/src/utils/dev.ts +2 -2
- package/src/utils/stack-info.ts +2 -2
- package/src/utils/tests/fixture.ts +2 -1
- package/src/utils/tests/otel.ts +8 -7
- package/docs/api/index.md +0 -3
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
- package/docs/building-with-livestore/crud/index.md +0 -5
- package/docs/building-with-livestore/data-modeling/index.md +0 -1
- package/docs/building-with-livestore/debugging/index.md +0 -17
- package/docs/building-with-livestore/devtools/index.md +0 -79
- package/docs/building-with-livestore/events/index.md +0 -355
- package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -208
- package/docs/building-with-livestore/production-checklist/index.md +0 -5
- package/docs/building-with-livestore/reactivity-system/index.md +0 -202
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
- package/docs/building-with-livestore/state/materializers/index.md +0 -300
- package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
- package/docs/building-with-livestore/state/sqlite/index.md +0 -45
- package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
- package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
- package/docs/building-with-livestore/store/index.md +0 -281
- package/docs/building-with-livestore/syncing/index.md +0 -136
- package/docs/building-with-livestore/tools/cli/index.md +0 -177
- package/docs/building-with-livestore/tools/mcp/index.md +0 -187
- package/docs/examples/cloudflare-adapter/index.md +0 -44
- package/docs/examples/expo-adapter/index.md +0 -44
- package/docs/examples/index.md +0 -55
- package/docs/examples/node-adapter/index.md +0 -44
- package/docs/examples/web-adapter/index.md +0 -52
- package/docs/framework-integrations/custom-elements/index.md +0 -142
- package/docs/framework-integrations/react-integration/index.md +0 -918
- package/docs/framework-integrations/solid-integration/index.md +0 -293
- package/docs/framework-integrations/svelte-integration/index.md +0 -42
- package/docs/framework-integrations/vue-integration/index.md +0 -294
- package/docs/getting-started/expo/index.md +0 -736
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -573
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -209
- package/docs/llms.txt +0 -147
- package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
- package/docs/misc/FAQ/index.md +0 -37
- package/docs/misc/community/index.md +0 -88
- package/docs/misc/credits/index.md +0 -14
- package/docs/misc/design-partners/index.md +0 -13
- package/docs/misc/package-management/index.md +0 -21
- package/docs/misc/performance/index.md +0 -25
- package/docs/misc/resources/index.md +0 -46
- package/docs/misc/state-of-the-project/index.md +0 -37
- package/docs/misc/troubleshooting/index.md +0 -82
- package/docs/overview/concepts/index.md +0 -78
- package/docs/overview/how-livestore-works/index.md +0 -56
- package/docs/overview/introduction/index.md +0 -5
- package/docs/overview/technology-comparison/index.md +0 -40
- package/docs/overview/when-livestore/index.md +0 -81
- package/docs/overview/why-livestore/index.md +0 -5
- package/docs/patterns/ai/index.md +0 -15
- package/docs/patterns/anonymous-user-transition/index.md +0 -10
- package/docs/patterns/app-evolution/index.md +0 -72
- package/docs/patterns/auth/index.md +0 -226
- package/docs/patterns/effect/index.md +0 -1495
- package/docs/patterns/encryption/index.md +0 -6
- package/docs/patterns/external-data/index.md +0 -5
- package/docs/patterns/file-management/index.md +0 -11
- package/docs/patterns/file-structure/index.md +0 -14
- package/docs/patterns/list-ordering/index.md +0 -369
- package/docs/patterns/offline/index.md +0 -32
- package/docs/patterns/orm/index.md +0 -18
- package/docs/patterns/presence/index.md +0 -11
- package/docs/patterns/rich-text-editing/index.md +0 -11
- package/docs/patterns/server-side-clients/index.md +0 -97
- package/docs/patterns/side-effects/index.md +0 -11
- package/docs/patterns/state-machines/index.md +0 -11
- package/docs/patterns/storybook/index.md +0 -192
- package/docs/patterns/undo-redo/index.md +0 -9
- package/docs/patterns/version-control/index.md +0 -8
- package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
- package/docs/platform-adapters/electron-adapter/index.md +0 -15
- package/docs/platform-adapters/expo-adapter/index.md +0 -245
- package/docs/platform-adapters/node-adapter/index.md +0 -160
- package/docs/platform-adapters/tauri-adapter/index.md +0 -15
- package/docs/platform-adapters/web-adapter/index.md +0 -218
- package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
- package/docs/sustainable-open-source/contributing/info/index.md +0 -63
- package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
- package/docs/sustainable-open-source/sponsoring/index.md +0 -104
- package/docs/sync-providers/cloudflare/index.md +0 -773
- package/docs/sync-providers/custom/index.md +0 -65
- package/docs/sync-providers/electricsql/index.md +0 -159
- package/docs/sync-providers/s2/index.md +0 -230
- package/docs/tutorial/0-welcome/index.md +0 -48
- package/docs/tutorial/1-setup-starter-project/index.md +0 -105
- package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
- package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
- package/docs/tutorial/5-expand-business-logic/index.md +0 -174
- package/docs/tutorial/6-persist-ui-state/index.md +0 -453
- package/docs/tutorial/7-next-steps/index.md +0 -22
- package/docs/understanding-livestore/design-decisions/index.md +0 -33
- package/docs/understanding-livestore/event-sourcing/index.md +0 -40
|
@@ -1,918 +0,0 @@
|
|
|
1
|
-
# React integration for LiveStore
|
|
2
|
-
|
|
3
|
-
While LiveStore is framework agnostic, the `@livestore/react` package provides a first-class integration with React.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- High performance
|
|
8
|
-
- Fine-grained reactivity (using LiveStore's signals-based reactivity system)
|
|
9
|
-
- Instant, synchronous query results (without the need for `useEffect` and `isLoading` checks)
|
|
10
|
-
- Transactional state transitions (via `batchUpdates`)
|
|
11
|
-
- Also supports Expo / React Native via `@livestore/adapter-expo`
|
|
12
|
-
|
|
13
|
-
## API
|
|
14
|
-
|
|
15
|
-
### `LiveStoreProvider`
|
|
16
|
-
|
|
17
|
-
In order to use LiveStore with React, you need to wrap your application in a `LiveStoreProvider`.
|
|
18
|
-
|
|
19
|
-
## `reference/framework-integrations/react/provider.tsx`
|
|
20
|
-
|
|
21
|
-
```tsx filename="reference/framework-integrations/react/provider.tsx"
|
|
22
|
-
|
|
23
|
-
const adapter = makeInMemoryAdapter()
|
|
24
|
-
|
|
25
|
-
export const Root: FC<{ children: ReactNode }> = ({ children }) => (
|
|
26
|
-
<LiveStoreProvider schema={schema} adapter={adapter} batchUpdates={batchUpdates}>
|
|
27
|
-
{children}
|
|
28
|
-
</LiveStoreProvider>
|
|
29
|
-
)
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
33
|
-
|
|
34
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
35
|
-
|
|
36
|
-
export const tables = {
|
|
37
|
-
todos: State.SQLite.table({
|
|
38
|
-
name: 'todos',
|
|
39
|
-
columns: {
|
|
40
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
41
|
-
text: State.SQLite.text(),
|
|
42
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
43
|
-
},
|
|
44
|
-
}),
|
|
45
|
-
uiState: State.SQLite.clientDocument({
|
|
46
|
-
name: 'UiState',
|
|
47
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
48
|
-
default: { value: { text: '' } },
|
|
49
|
-
}),
|
|
50
|
-
} as const
|
|
51
|
-
|
|
52
|
-
export const events = {
|
|
53
|
-
todoCreated: Events.synced({
|
|
54
|
-
name: 'v1.TodoCreated',
|
|
55
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
56
|
-
}),
|
|
57
|
-
} as const
|
|
58
|
-
|
|
59
|
-
const materializers = State.SQLite.materializers(events, {
|
|
60
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
61
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
62
|
-
),
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
66
|
-
|
|
67
|
-
export const schema = makeSchema({ events, state })
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
#### Logging
|
|
71
|
-
|
|
72
|
-
`LiveStoreProvider` accepts optional logging configuration:
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
75
|
-
|
|
76
|
-
<LiveStoreProvider
|
|
77
|
-
schema={schema}
|
|
78
|
-
adapter={adapter}
|
|
79
|
-
batchUpdates={batchUpdates}
|
|
80
|
-
// Optional: swap the logger implementation
|
|
81
|
-
logger={Logger.prettyWithThread('app')}
|
|
82
|
-
// Optional: set minimum log level (use LogLevel.None to disable)
|
|
83
|
-
logLevel={LogLevel.Info}
|
|
84
|
-
>
|
|
85
|
-
<App />
|
|
86
|
-
</LiveStoreProvider>
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
For scenarios where you have an existing store instance, you can manually create a `LiveStoreContext.Provider`:
|
|
90
|
-
|
|
91
|
-
## `reference/framework-integrations/react/context-provider.tsx`
|
|
92
|
-
|
|
93
|
-
```tsx filename="reference/framework-integrations/react/context-provider.tsx"
|
|
94
|
-
|
|
95
|
-
declare const store: Store & ReactApi
|
|
96
|
-
|
|
97
|
-
export const Root: FC<{ children: ReactNode }> = ({ children }) => (
|
|
98
|
-
<LiveStoreContext.Provider value={{ stage: 'running', store }}>{children}</LiveStoreContext.Provider>
|
|
99
|
-
)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### useStore
|
|
103
|
-
|
|
104
|
-
## `reference/framework-integrations/react/use-store.tsx`
|
|
105
|
-
|
|
106
|
-
```tsx filename="reference/framework-integrations/react/use-store.tsx"
|
|
107
|
-
|
|
108
|
-
export const MyComponent: FC = () => {
|
|
109
|
-
const { store } = useStore()
|
|
110
|
-
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
store.commit(events.todoCreated({ id: '1', text: 'Hello, world!' }))
|
|
113
|
-
}, [store])
|
|
114
|
-
|
|
115
|
-
return <div>...</div>
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
120
|
-
|
|
121
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
122
|
-
|
|
123
|
-
export const tables = {
|
|
124
|
-
todos: State.SQLite.table({
|
|
125
|
-
name: 'todos',
|
|
126
|
-
columns: {
|
|
127
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
128
|
-
text: State.SQLite.text(),
|
|
129
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
uiState: State.SQLite.clientDocument({
|
|
133
|
-
name: 'UiState',
|
|
134
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
135
|
-
default: { value: { text: '' } },
|
|
136
|
-
}),
|
|
137
|
-
} as const
|
|
138
|
-
|
|
139
|
-
export const events = {
|
|
140
|
-
todoCreated: Events.synced({
|
|
141
|
-
name: 'v1.TodoCreated',
|
|
142
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
143
|
-
}),
|
|
144
|
-
} as const
|
|
145
|
-
|
|
146
|
-
const materializers = State.SQLite.materializers(events, {
|
|
147
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
148
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
149
|
-
),
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
153
|
-
|
|
154
|
-
export const schema = makeSchema({ events, state })
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### useQuery
|
|
158
|
-
|
|
159
|
-
## `reference/framework-integrations/react/use-query.tsx`
|
|
160
|
-
|
|
161
|
-
```tsx filename="reference/framework-integrations/react/use-query.tsx"
|
|
162
|
-
|
|
163
|
-
const query$ = queryDb(tables.todos.where({ completed: true }).orderBy('id', 'desc'), {
|
|
164
|
-
label: 'completedTodos',
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
export const CompletedTodos: FC = () => {
|
|
168
|
-
const { store } = useStore()
|
|
169
|
-
const todos = store.useQuery(query$)
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<div>
|
|
173
|
-
{todos.map((todo) => (
|
|
174
|
-
<div key={todo.id}>{todo.text}</div>
|
|
175
|
-
))}
|
|
176
|
-
</div>
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
182
|
-
|
|
183
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
184
|
-
|
|
185
|
-
export const tables = {
|
|
186
|
-
todos: State.SQLite.table({
|
|
187
|
-
name: 'todos',
|
|
188
|
-
columns: {
|
|
189
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
190
|
-
text: State.SQLite.text(),
|
|
191
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
192
|
-
},
|
|
193
|
-
}),
|
|
194
|
-
uiState: State.SQLite.clientDocument({
|
|
195
|
-
name: 'UiState',
|
|
196
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
197
|
-
default: { value: { text: '' } },
|
|
198
|
-
}),
|
|
199
|
-
} as const
|
|
200
|
-
|
|
201
|
-
export const events = {
|
|
202
|
-
todoCreated: Events.synced({
|
|
203
|
-
name: 'v1.TodoCreated',
|
|
204
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
205
|
-
}),
|
|
206
|
-
} as const
|
|
207
|
-
|
|
208
|
-
const materializers = State.SQLite.materializers(events, {
|
|
209
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
210
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
211
|
-
),
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
215
|
-
|
|
216
|
-
export const schema = makeSchema({ events, state })
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### useClientDocument
|
|
220
|
-
|
|
221
|
-
## `reference/framework-integrations/react/use-client-document.tsx`
|
|
222
|
-
|
|
223
|
-
```tsx filename="reference/framework-integrations/react/use-client-document.tsx"
|
|
224
|
-
|
|
225
|
-
export const TodoItem: FC<{ id: string }> = ({ id }) => {
|
|
226
|
-
const { store } = useStore()
|
|
227
|
-
const [todo, updateTodo] = store.useClientDocument(tables.uiState, id)
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<button type="button" onClick={() => updateTodo({ text: 'Hello, world!' })}>
|
|
231
|
-
{todo.text}
|
|
232
|
-
</button>
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
238
|
-
|
|
239
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
240
|
-
|
|
241
|
-
export const tables = {
|
|
242
|
-
todos: State.SQLite.table({
|
|
243
|
-
name: 'todos',
|
|
244
|
-
columns: {
|
|
245
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
246
|
-
text: State.SQLite.text(),
|
|
247
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
250
|
-
uiState: State.SQLite.clientDocument({
|
|
251
|
-
name: 'UiState',
|
|
252
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
253
|
-
default: { value: { text: '' } },
|
|
254
|
-
}),
|
|
255
|
-
} as const
|
|
256
|
-
|
|
257
|
-
export const events = {
|
|
258
|
-
todoCreated: Events.synced({
|
|
259
|
-
name: 'v1.TodoCreated',
|
|
260
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
261
|
-
}),
|
|
262
|
-
} as const
|
|
263
|
-
|
|
264
|
-
const materializers = State.SQLite.materializers(events, {
|
|
265
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
266
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
267
|
-
),
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
271
|
-
|
|
272
|
-
export const schema = makeSchema({ events, state })
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## Usage with ...
|
|
276
|
-
|
|
277
|
-
### Vite
|
|
278
|
-
|
|
279
|
-
LiveStore works with Vite out of the box.
|
|
280
|
-
|
|
281
|
-
### Tanstack Start
|
|
282
|
-
|
|
283
|
-
LiveStore works with Tanstack Start out of the box.
|
|
284
|
-
|
|
285
|
-
#### Notes
|
|
286
|
-
|
|
287
|
-
When using LiveStore with TanStack Start, it's crucial to place `LiveStoreProvider` in the correct location to avoid remounting on navigation.
|
|
288
|
-
|
|
289
|
-
:::warn
|
|
290
|
-
**Do NOT place `LiveStoreProvider` inside `shellComponent`**. The `shellComponent` can be re-rendered on navigation, causing LiveStore to remount and show the loading screen on every page transition.
|
|
291
|
-
:::
|
|
292
|
-
|
|
293
|
-
#### Correct pattern
|
|
294
|
-
|
|
295
|
-
Use the `component` prop on `createRootRoute` for `LiveStoreProvider`:
|
|
296
|
-
|
|
297
|
-
```tsx
|
|
298
|
-
|
|
299
|
-
export const Route = createRootRoute({
|
|
300
|
-
shellComponent: RootShell, // HTML structure only - NO state or providers
|
|
301
|
-
component: RootComponent, // App shell - LiveStoreProvider goes HERE
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// HTML document shell - keep this stateless
|
|
305
|
-
function RootShell({ children }: { children: React.ReactNode }) {
|
|
306
|
-
return (
|
|
307
|
-
<html lang="en">
|
|
308
|
-
<head><HeadContent /></head>
|
|
309
|
-
<body>
|
|
310
|
-
{children}
|
|
311
|
-
<Scripts />
|
|
312
|
-
</body>
|
|
313
|
-
</html>
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// App shell - persists across SPA navigation
|
|
318
|
-
function RootComponent() {
|
|
319
|
-
return (
|
|
320
|
-
<LiveStoreProvider
|
|
321
|
-
schema={schema}
|
|
322
|
-
adapter={adapter}
|
|
323
|
-
batchUpdates={batchUpdates}
|
|
324
|
-
>
|
|
325
|
-
<Outlet />
|
|
326
|
-
</LiveStoreProvider>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
#### Why this matters
|
|
332
|
-
|
|
333
|
-
TanStack Start's `shellComponent` is designed for SSR HTML streaming and may be re-evaluated on server requests during navigation. When `LiveStoreProvider` is placed there:
|
|
334
|
-
|
|
335
|
-
- The WebSocket connection is re-established on each navigation
|
|
336
|
-
- The "Loading LiveStore" screen appears
|
|
337
|
-
- All LiveStore state is re-initialized
|
|
338
|
-
|
|
339
|
-
The `component` prop creates a React component that stays mounted during client-side SPA navigation, preserving LiveStore's connection and state.
|
|
340
|
-
|
|
341
|
-
#### Debugging
|
|
342
|
-
|
|
343
|
-
If you see the loading screen on every navigation, check your server logs. Multiple "Launching WebSocket" messages indicate `LiveStoreProvider` is remounting incorrectly.
|
|
344
|
-
|
|
345
|
-
### Expo / React Native
|
|
346
|
-
|
|
347
|
-
LiveStore has a first-class integration with Expo / React Native via `@livestore/adapter-expo`.
|
|
348
|
-
|
|
349
|
-
### Next.js
|
|
350
|
-
|
|
351
|
-
Given various Next.js limitations, LiveStore doesn't yet work with Next.js out of the box.
|
|
352
|
-
|
|
353
|
-
## Multi-Store
|
|
354
|
-
|
|
355
|
-
The multi-store API enables managing multiple stores within a single React application. This is useful for:
|
|
356
|
-
|
|
357
|
-
- **Partial data synchronization** - Load only the data you need, when you need it
|
|
358
|
-
- **Multi-tenant applications** - Separate stores for each workspace, organization, or project (like Slack workspaces, Notion pages, or Linear teams)
|
|
359
|
-
|
|
360
|
-
## `reference/framework-integrations/react/multi-store/minimal.tsx`
|
|
361
|
-
|
|
362
|
-
```tsx filename="reference/framework-integrations/react/multi-store/minimal.tsx"
|
|
363
|
-
|
|
364
|
-
const issueStoreOptions = (issueId: string) =>
|
|
365
|
-
storeOptions({
|
|
366
|
-
storeId: `issue-${issueId}`,
|
|
367
|
-
schema,
|
|
368
|
-
adapter: makeInMemoryAdapter(),
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
export function App() {
|
|
372
|
-
const [registry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
|
|
373
|
-
return (
|
|
374
|
-
<StoreRegistryProvider storeRegistry={registry}>
|
|
375
|
-
<IssueView />
|
|
376
|
-
</StoreRegistryProvider>
|
|
377
|
-
)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function IssueView() {
|
|
381
|
-
const store = useStore(issueStoreOptions('abc123'))
|
|
382
|
-
const [issue] = store.useQuery(queryDb(tables.issue.select()))
|
|
383
|
-
return <div>{issue?.title}</div>
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
388
|
-
|
|
389
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
390
|
-
|
|
391
|
-
// Event definitions
|
|
392
|
-
export const events = {
|
|
393
|
-
issueCreated: Events.synced({
|
|
394
|
-
name: 'v1.IssueCreated',
|
|
395
|
-
schema: Schema.Struct({
|
|
396
|
-
id: Schema.String,
|
|
397
|
-
title: Schema.String,
|
|
398
|
-
status: Schema.Literal('todo', 'done'),
|
|
399
|
-
}),
|
|
400
|
-
}),
|
|
401
|
-
issueStatusChanged: Events.synced({
|
|
402
|
-
name: 'v1.IssueStatusChanged',
|
|
403
|
-
schema: Schema.Struct({
|
|
404
|
-
id: Schema.String,
|
|
405
|
-
status: Schema.Literal('todo', 'done'),
|
|
406
|
-
}),
|
|
407
|
-
}),
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// State definition
|
|
411
|
-
export const tables = {
|
|
412
|
-
issue: State.SQLite.table({
|
|
413
|
-
name: 'issue',
|
|
414
|
-
columns: {
|
|
415
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
416
|
-
title: State.SQLite.text(),
|
|
417
|
-
status: State.SQLite.text(),
|
|
418
|
-
},
|
|
419
|
-
}),
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const materializers = State.SQLite.materializers(events, {
|
|
423
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
424
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
428
|
-
|
|
429
|
-
export const schema = makeSchema({ events, state })
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
:::caution[Experimental API]
|
|
433
|
-
The Multi-Store API is still early in its development.
|
|
434
|
-
|
|
435
|
-
If you have feedback or questions about this API, please don't hesitate to comment on the [RFC](https://github.com/livestorejs/livestore/pull/585)
|
|
436
|
-
:::
|
|
437
|
-
|
|
438
|
-
### Core Concepts
|
|
439
|
-
|
|
440
|
-
The multi-store API introduces four main primitives:
|
|
441
|
-
|
|
442
|
-
- **StoreRegistry** - Manages and caches all store instances with automatic garbage collection
|
|
443
|
-
- **useStore()** - Suspense-enabled hook for accessing individual store instances
|
|
444
|
-
- **storeOptions()** - Type-safe way to define reusable store configurations
|
|
445
|
-
|
|
446
|
-
Stores are cached by their `storeId` and automatically disposed after being unused for a configurable duration (`unusedCacheTime`)
|
|
447
|
-
|
|
448
|
-
### Setting Up
|
|
449
|
-
|
|
450
|
-
First, define your re-usable store configuration using `storeOptions()`:
|
|
451
|
-
|
|
452
|
-
## `reference/framework-integrations/react/multi-store/store.ts`
|
|
453
|
-
|
|
454
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
455
|
-
|
|
456
|
-
// Define reusable store configuration with storeOptions()
|
|
457
|
-
// This helper provides type safety and can be reused across your app
|
|
458
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
459
|
-
storeOptions({
|
|
460
|
-
storeId: `issue-${issueId}`,
|
|
461
|
-
schema,
|
|
462
|
-
adapter: makeInMemoryAdapter(),
|
|
463
|
-
})
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
467
|
-
|
|
468
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
469
|
-
|
|
470
|
-
// Event definitions
|
|
471
|
-
export const events = {
|
|
472
|
-
issueCreated: Events.synced({
|
|
473
|
-
name: 'v1.IssueCreated',
|
|
474
|
-
schema: Schema.Struct({
|
|
475
|
-
id: Schema.String,
|
|
476
|
-
title: Schema.String,
|
|
477
|
-
status: Schema.Literal('todo', 'done'),
|
|
478
|
-
}),
|
|
479
|
-
}),
|
|
480
|
-
issueStatusChanged: Events.synced({
|
|
481
|
-
name: 'v1.IssueStatusChanged',
|
|
482
|
-
schema: Schema.Struct({
|
|
483
|
-
id: Schema.String,
|
|
484
|
-
status: Schema.Literal('todo', 'done'),
|
|
485
|
-
}),
|
|
486
|
-
}),
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// State definition
|
|
490
|
-
export const tables = {
|
|
491
|
-
issue: State.SQLite.table({
|
|
492
|
-
name: 'issue',
|
|
493
|
-
columns: {
|
|
494
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
495
|
-
title: State.SQLite.text(),
|
|
496
|
-
status: State.SQLite.text(),
|
|
497
|
-
},
|
|
498
|
-
}),
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const materializers = State.SQLite.materializers(events, {
|
|
502
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
503
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
507
|
-
|
|
508
|
-
export const schema = makeSchema({ events, state })
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
Then create a `StoreRegistry` and provide it to your app:
|
|
512
|
-
|
|
513
|
-
## `reference/framework-integrations/react/multi-store/App.tsx`
|
|
514
|
-
|
|
515
|
-
```tsx filename="reference/framework-integrations/react/multi-store/App.tsx"
|
|
516
|
-
|
|
517
|
-
export function App({ children }: { children: ReactNode }) {
|
|
518
|
-
const [storeRegistry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
|
|
519
|
-
|
|
520
|
-
return <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### Using Stores
|
|
525
|
-
|
|
526
|
-
Use the `useStore()` hook to load or get a store instance. It suspends until the store is loaded:
|
|
527
|
-
|
|
528
|
-
## `reference/framework-integrations/react/multi-store/IssueView.tsx`
|
|
529
|
-
|
|
530
|
-
```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
|
|
531
|
-
|
|
532
|
-
export function IssueView({ issueId }: { issueId: string }) {
|
|
533
|
-
// useStore() suspends the component until the store is loaded
|
|
534
|
-
// If the same store was already loaded, it returns immediately
|
|
535
|
-
const issueStore = useStore(issueStoreOptions(issueId))
|
|
536
|
-
|
|
537
|
-
// Query data from the store
|
|
538
|
-
const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
|
|
539
|
-
|
|
540
|
-
if (!issue) return <div>Issue not found</div>
|
|
541
|
-
|
|
542
|
-
return (
|
|
543
|
-
<div>
|
|
544
|
-
<h3>{issue.title}</h3>
|
|
545
|
-
<p>Status: {issue.status}</p>
|
|
546
|
-
</div>
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Wrap with Suspense and ErrorBoundary for loading and error states
|
|
551
|
-
export function IssueViewWithSuspense({ issueId }: { issueId: string }) {
|
|
552
|
-
return (
|
|
553
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
554
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
555
|
-
<IssueView issueId={issueId} />
|
|
556
|
-
</Suspense>
|
|
557
|
-
</ErrorBoundary>
|
|
558
|
-
)
|
|
559
|
-
}
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
563
|
-
|
|
564
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
565
|
-
|
|
566
|
-
// Event definitions
|
|
567
|
-
export const events = {
|
|
568
|
-
issueCreated: Events.synced({
|
|
569
|
-
name: 'v1.IssueCreated',
|
|
570
|
-
schema: Schema.Struct({
|
|
571
|
-
id: Schema.String,
|
|
572
|
-
title: Schema.String,
|
|
573
|
-
status: Schema.Literal('todo', 'done'),
|
|
574
|
-
}),
|
|
575
|
-
}),
|
|
576
|
-
issueStatusChanged: Events.synced({
|
|
577
|
-
name: 'v1.IssueStatusChanged',
|
|
578
|
-
schema: Schema.Struct({
|
|
579
|
-
id: Schema.String,
|
|
580
|
-
status: Schema.Literal('todo', 'done'),
|
|
581
|
-
}),
|
|
582
|
-
}),
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// State definition
|
|
586
|
-
export const tables = {
|
|
587
|
-
issue: State.SQLite.table({
|
|
588
|
-
name: 'issue',
|
|
589
|
-
columns: {
|
|
590
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
591
|
-
title: State.SQLite.text(),
|
|
592
|
-
status: State.SQLite.text(),
|
|
593
|
-
},
|
|
594
|
-
}),
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const materializers = State.SQLite.materializers(events, {
|
|
598
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
599
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
603
|
-
|
|
604
|
-
export const schema = makeSchema({ events, state })
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
### `reference/framework-integrations/react/multi-store/store.ts`
|
|
608
|
-
|
|
609
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
610
|
-
|
|
611
|
-
// Define reusable store configuration with storeOptions()
|
|
612
|
-
// This helper provides type safety and can be reused across your app
|
|
613
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
614
|
-
storeOptions({
|
|
615
|
-
storeId: `issue-${issueId}`,
|
|
616
|
-
schema,
|
|
617
|
-
adapter: makeInMemoryAdapter(),
|
|
618
|
-
})
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
### Multiple Instances
|
|
622
|
-
|
|
623
|
-
You can create multiple instances of the same store type by using different `storeId` values:
|
|
624
|
-
|
|
625
|
-
## `reference/framework-integrations/react/multi-store/IssueList.tsx`
|
|
626
|
-
|
|
627
|
-
```tsx filename="reference/framework-integrations/react/multi-store/IssueList.tsx"
|
|
628
|
-
|
|
629
|
-
function IssueCard({ issueId }: { issueId: string }) {
|
|
630
|
-
// Each call to useStore with a different storeId loads/gets a separate store instance
|
|
631
|
-
const issueStore = useStore(issueStoreOptions(issueId))
|
|
632
|
-
const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
|
|
633
|
-
|
|
634
|
-
if (!issue) return <div>Issue not found</div>
|
|
635
|
-
|
|
636
|
-
return (
|
|
637
|
-
<div>
|
|
638
|
-
<h4>{issue.title}</h4>
|
|
639
|
-
<p>Store ID: {issueStore.storeId}</p>
|
|
640
|
-
<p>Status: {issue.status}</p>
|
|
641
|
-
</div>
|
|
642
|
-
)
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Component that displays multiple independent store instances with shared error and loading states
|
|
646
|
-
export function IssueList() {
|
|
647
|
-
const issueIds = ['issue-1', 'issue-2', 'issue-3']
|
|
648
|
-
|
|
649
|
-
return (
|
|
650
|
-
<div>
|
|
651
|
-
<h3>Issues</h3>
|
|
652
|
-
<ErrorBoundary fallback={<div>Error loading issues</div>}>
|
|
653
|
-
<Suspense fallback={<div>Loading issues...</div>}>
|
|
654
|
-
{issueIds.map((id) => (
|
|
655
|
-
<IssueCard key={id} issueId={id} />
|
|
656
|
-
))}
|
|
657
|
-
</Suspense>
|
|
658
|
-
</ErrorBoundary>
|
|
659
|
-
</div>
|
|
660
|
-
)
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
665
|
-
|
|
666
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
667
|
-
|
|
668
|
-
// Event definitions
|
|
669
|
-
export const events = {
|
|
670
|
-
issueCreated: Events.synced({
|
|
671
|
-
name: 'v1.IssueCreated',
|
|
672
|
-
schema: Schema.Struct({
|
|
673
|
-
id: Schema.String,
|
|
674
|
-
title: Schema.String,
|
|
675
|
-
status: Schema.Literal('todo', 'done'),
|
|
676
|
-
}),
|
|
677
|
-
}),
|
|
678
|
-
issueStatusChanged: Events.synced({
|
|
679
|
-
name: 'v1.IssueStatusChanged',
|
|
680
|
-
schema: Schema.Struct({
|
|
681
|
-
id: Schema.String,
|
|
682
|
-
status: Schema.Literal('todo', 'done'),
|
|
683
|
-
}),
|
|
684
|
-
}),
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// State definition
|
|
688
|
-
export const tables = {
|
|
689
|
-
issue: State.SQLite.table({
|
|
690
|
-
name: 'issue',
|
|
691
|
-
columns: {
|
|
692
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
693
|
-
title: State.SQLite.text(),
|
|
694
|
-
status: State.SQLite.text(),
|
|
695
|
-
},
|
|
696
|
-
}),
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const materializers = State.SQLite.materializers(events, {
|
|
700
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
701
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
705
|
-
|
|
706
|
-
export const schema = makeSchema({ events, state })
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
### `reference/framework-integrations/react/multi-store/store.ts`
|
|
710
|
-
|
|
711
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
712
|
-
|
|
713
|
-
// Define reusable store configuration with storeOptions()
|
|
714
|
-
// This helper provides type safety and can be reused across your app
|
|
715
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
716
|
-
storeOptions({
|
|
717
|
-
storeId: `issue-${issueId}`,
|
|
718
|
-
schema,
|
|
719
|
-
adapter: makeInMemoryAdapter(),
|
|
720
|
-
})
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
Each store instance is completely isolated with its own data, event log, and synchronization state.
|
|
724
|
-
|
|
725
|
-
### Preloading
|
|
726
|
-
|
|
727
|
-
When you know a store will be needed soon, you can preload it in advance:
|
|
728
|
-
|
|
729
|
-
## `reference/framework-integrations/react/multi-store/PreloadedIssue.tsx`
|
|
730
|
-
|
|
731
|
-
```tsx filename="reference/framework-integrations/react/multi-store/PreloadedIssue.tsx"
|
|
732
|
-
|
|
733
|
-
export function PreloadedIssue({ issueId }: { issueId: string }) {
|
|
734
|
-
const [showIssue, setShowIssue] = useState(false)
|
|
735
|
-
const storeRegistry = useStoreRegistry()
|
|
736
|
-
|
|
737
|
-
// Preload the store when user hovers (before they click)
|
|
738
|
-
const handleMouseEnter = () => {
|
|
739
|
-
storeRegistry.preload(issueStoreOptions(issueId))
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return (
|
|
743
|
-
<div>
|
|
744
|
-
{!showIssue ? (
|
|
745
|
-
<button type="button" onMouseEnter={handleMouseEnter} onClick={() => setShowIssue(true)}>
|
|
746
|
-
Show Issue
|
|
747
|
-
</button>
|
|
748
|
-
) : (
|
|
749
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
750
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
751
|
-
<IssueView issueId={issueId} />
|
|
752
|
-
</Suspense>
|
|
753
|
-
</ErrorBoundary>
|
|
754
|
-
)}
|
|
755
|
-
</div>
|
|
756
|
-
)
|
|
757
|
-
}
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
### `reference/framework-integrations/react/multi-store/IssueView.tsx`
|
|
761
|
-
|
|
762
|
-
```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
|
|
763
|
-
|
|
764
|
-
export function IssueView({ issueId }: { issueId: string }) {
|
|
765
|
-
// useStore() suspends the component until the store is loaded
|
|
766
|
-
// If the same store was already loaded, it returns immediately
|
|
767
|
-
const issueStore = useStore(issueStoreOptions(issueId))
|
|
768
|
-
|
|
769
|
-
// Query data from the store
|
|
770
|
-
const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
|
|
771
|
-
|
|
772
|
-
if (!issue) return <div>Issue not found</div>
|
|
773
|
-
|
|
774
|
-
return (
|
|
775
|
-
<div>
|
|
776
|
-
<h3>{issue.title}</h3>
|
|
777
|
-
<p>Status: {issue.status}</p>
|
|
778
|
-
</div>
|
|
779
|
-
)
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Wrap with Suspense and ErrorBoundary for loading and error states
|
|
783
|
-
export function IssueViewWithSuspense({ issueId }: { issueId: string }) {
|
|
784
|
-
return (
|
|
785
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
786
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
787
|
-
<IssueView issueId={issueId} />
|
|
788
|
-
</Suspense>
|
|
789
|
-
</ErrorBoundary>
|
|
790
|
-
)
|
|
791
|
-
}
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
795
|
-
|
|
796
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
797
|
-
|
|
798
|
-
// Event definitions
|
|
799
|
-
export const events = {
|
|
800
|
-
issueCreated: Events.synced({
|
|
801
|
-
name: 'v1.IssueCreated',
|
|
802
|
-
schema: Schema.Struct({
|
|
803
|
-
id: Schema.String,
|
|
804
|
-
title: Schema.String,
|
|
805
|
-
status: Schema.Literal('todo', 'done'),
|
|
806
|
-
}),
|
|
807
|
-
}),
|
|
808
|
-
issueStatusChanged: Events.synced({
|
|
809
|
-
name: 'v1.IssueStatusChanged',
|
|
810
|
-
schema: Schema.Struct({
|
|
811
|
-
id: Schema.String,
|
|
812
|
-
status: Schema.Literal('todo', 'done'),
|
|
813
|
-
}),
|
|
814
|
-
}),
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// State definition
|
|
818
|
-
export const tables = {
|
|
819
|
-
issue: State.SQLite.table({
|
|
820
|
-
name: 'issue',
|
|
821
|
-
columns: {
|
|
822
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
823
|
-
title: State.SQLite.text(),
|
|
824
|
-
status: State.SQLite.text(),
|
|
825
|
-
},
|
|
826
|
-
}),
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const materializers = State.SQLite.materializers(events, {
|
|
830
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
831
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
832
|
-
})
|
|
833
|
-
|
|
834
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
835
|
-
|
|
836
|
-
export const schema = makeSchema({ events, state })
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
### `reference/framework-integrations/react/multi-store/store.ts`
|
|
840
|
-
|
|
841
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
842
|
-
|
|
843
|
-
// Define reusable store configuration with storeOptions()
|
|
844
|
-
// This helper provides type safety and can be reused across your app
|
|
845
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
846
|
-
storeOptions({
|
|
847
|
-
storeId: `issue-${issueId}`,
|
|
848
|
-
schema,
|
|
849
|
-
adapter: makeInMemoryAdapter(),
|
|
850
|
-
})
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
This warms up the cache so the store is ready when the user navigates to it.
|
|
854
|
-
|
|
855
|
-
### StoreId Guidelines
|
|
856
|
-
|
|
857
|
-
When creating `storeId` values:
|
|
858
|
-
|
|
859
|
-
- **Use namespaces** - Prefix with the entity type (e.g., `workspace-abc123`, `issue-456`) to avoid collisions between different store types and improve debugging
|
|
860
|
-
- **Globally unique** - Prefer globally unique IDs (e.g., nanoid) to prevent collisions
|
|
861
|
-
- **Keep them stable** - The same entity should always use the same `storeId` across renders
|
|
862
|
-
- **Sanitize user input** - If incorporating user data, be sure to validate/sanitize to prevent injection attacks
|
|
863
|
-
- **Document your conventions** - Document your conventions and special IDs like `user-current` as they're part of your API contract
|
|
864
|
-
|
|
865
|
-
### API Reference
|
|
866
|
-
|
|
867
|
-
#### `storeOptions(options)`
|
|
868
|
-
|
|
869
|
-
Defines reusable store configuration with type safety. Returns options that can be passed to `useStore()` or `registry.preload()`.
|
|
870
|
-
|
|
871
|
-
Options:
|
|
872
|
-
- `storeId` - Unique identifier for this store instance (required)
|
|
873
|
-
- `schema` - The LiveStore schema (required)
|
|
874
|
-
- `adapter` - The platform adapter (required)
|
|
875
|
-
- `unusedCacheTime` - Time in milliseconds to keep unused stores in memory (default: 60_000 in browser, infinity in non-browser)
|
|
876
|
-
- `boot` - Function called when the store is first loaded
|
|
877
|
-
- `batchUpdates` - Function for batching React updates
|
|
878
|
-
- And other `CreateStoreOptions`
|
|
879
|
-
|
|
880
|
-
#### `new StoreRegistry(config?)`
|
|
881
|
-
|
|
882
|
-
Creates a registry that manages store instances.
|
|
883
|
-
|
|
884
|
-
Config:
|
|
885
|
-
- `defaultOptions` - Default options applied to all stores (can be overridden per-store)
|
|
886
|
-
|
|
887
|
-
#### `StoreRegistryProvider`
|
|
888
|
-
|
|
889
|
-
React context provider that supplies the registry to components.
|
|
890
|
-
|
|
891
|
-
Props:
|
|
892
|
-
- `storeRegistry` - The registry instance (required)
|
|
893
|
-
- `children` - React nodes (required)
|
|
894
|
-
|
|
895
|
-
#### `useStore(options)`
|
|
896
|
-
|
|
897
|
-
Hook that returns a store instance, suspending until it's loaded.
|
|
898
|
-
|
|
899
|
-
- Throws a Promise during loading (for React Suspense)
|
|
900
|
-
- Throws an Error if loading fails (for Error Boundaries)
|
|
901
|
-
- Returns the loaded store when ready
|
|
902
|
-
|
|
903
|
-
#### `useStoreRegistry()`
|
|
904
|
-
|
|
905
|
-
Returns the current `StoreRegistry` from context. Useful for advanced operations like preloading.
|
|
906
|
-
|
|
907
|
-
#### `registry.preload(options)`
|
|
908
|
-
|
|
909
|
-
Starts loading a store without suspending. Returns a Promise that resolves when loading completes (or rejects on error). This is a fire-and-forget operation.
|
|
910
|
-
|
|
911
|
-
### Complete Example
|
|
912
|
-
|
|
913
|
-
See the <a href={`https://github.com/livestorejs/livestore/tree/${getBranchName()}/examples/web-multi-store`}>Multi-Store example</a> for a complete working application demonstrating various multi-store patterns.
|
|
914
|
-
|
|
915
|
-
## Technical notes
|
|
916
|
-
|
|
917
|
-
- `@livestore/react` uses `React.useState` under the hood for `useQuery` / `useClientDocument` to bind LiveStore's reactivity to React's reactivity. Some libraries are using `React.useExternalSyncStore` for similar purposes but using `React.useState` in this case is more efficient and all that's needed for LiveStore.
|
|
918
|
-
- `@livestore/react` supports React Strict Mode.
|