@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.22
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/dist/.tsbuildinfo +1 -1
- package/dist/effect/LiveStore.d.ts +123 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +195 -1
- package/dist/effect/LiveStore.js.map +1 -1
- 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/mod.d.ts +1 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +190 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +244 -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 +380 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +50 -4
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +19 -0
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +13 -0
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +10 -25
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +23 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +20 -2
- package/dist/store/store.js.map +1 -1
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -2
- package/docs/building-with-livestore/crud/index.md +0 -2
- package/docs/building-with-livestore/data-modeling/index.md +29 -0
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -6
- package/docs/building-with-livestore/opentelemetry/index.md +25 -6
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +2 -2
- package/docs/building-with-livestore/state/sql-queries/index.md +22 -0
- package/docs/building-with-livestore/state/sqlite-schema/index.md +2 -2
- package/docs/building-with-livestore/store/index.md +344 -0
- package/docs/framework-integrations/react-integration/index.md +380 -361
- package/docs/framework-integrations/vue-integration/index.md +2 -2
- package/docs/getting-started/expo/index.md +189 -43
- package/docs/getting-started/react-web/index.md +77 -24
- package/docs/getting-started/vue/index.md +3 -3
- package/docs/index.md +1 -2
- package/docs/llms.txt +0 -1
- package/docs/misc/troubleshooting/index.md +3 -3
- package/docs/overview/how-livestore-works/index.md +1 -1
- package/docs/overview/introduction/index.md +409 -1
- package/docs/overview/why-livestore/index.md +108 -2
- package/docs/patterns/auth/index.md +185 -34
- package/docs/patterns/effect/index.md +11 -1
- package/docs/patterns/storybook/index.md +43 -26
- package/docs/platform-adapters/expo-adapter/index.md +36 -19
- package/docs/platform-adapters/web-adapter/index.md +71 -2
- package/docs/tutorial/1-setup-starter-project/index.md +5 -5
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +54 -35
- package/docs/tutorial/5-expand-business-logic/index.md +1 -1
- package/docs/tutorial/6-persist-ui-state/index.md +12 -12
- package/package.json +6 -6
- package/src/effect/LiveStore.ts +385 -3
- package/src/effect/mod.ts +13 -1
- package/src/mod.ts +1 -0
- package/src/store/StoreRegistry.test.ts +516 -0
- package/src/store/StoreRegistry.ts +393 -0
- package/src/store/create-store.ts +50 -4
- package/src/store/devtools.ts +15 -0
- package/src/store/store-types.ts +17 -5
- package/src/store/store.ts +25 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
|
@@ -7,26 +7,111 @@ While LiveStore is framework agnostic, the `@livestore/react` package provides a
|
|
|
7
7
|
- High performance
|
|
8
8
|
- Fine-grained reactivity (using LiveStore's signals-based reactivity system)
|
|
9
9
|
- Instant, synchronous query results (without the need for `useEffect` and `isLoading` checks)
|
|
10
|
+
- Supports multiple store instances
|
|
10
11
|
- Transactional state transitions (via `batchUpdates`)
|
|
11
12
|
- Also supports Expo / React Native via `@livestore/adapter-expo`
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Core Concepts
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
When using LiveStore in React, you'll primarily interact with these fundamental components:
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
- [**`StoreRegistry`**](#new-storeregistryconfig) - Manages store instances with automatic caching and disposal
|
|
19
|
+
- [**`<StoreRegistryProvider>`**](#storeregistryprovider) - React context provider that supplies the registry to components
|
|
20
|
+
- [**`useStore()`**](#usestoreoptions) - Suspense-enabled hook for accessing store instances
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Stores are cached by their `storeId` and automatically disposed after being unused for a configurable duration (`unusedCacheTime`).
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
## `reference/framework-integrations/react/multi-store/minimal.tsx`
|
|
25
|
+
|
|
26
|
+
```tsx filename="reference/framework-integrations/react/multi-store/minimal.tsx"
|
|
27
|
+
|
|
28
|
+
const issueStoreOptions = (issueId: string) =>
|
|
29
|
+
storeOptions({
|
|
30
|
+
storeId: `issue-${issueId}`,
|
|
31
|
+
schema,
|
|
32
|
+
adapter: makeInMemoryAdapter(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export function App() {
|
|
36
|
+
const [storeRegistry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
|
|
37
|
+
return (
|
|
38
|
+
<StoreRegistryProvider storeRegistry={storeRegistry}>
|
|
39
|
+
<IssueView />
|
|
40
|
+
</StoreRegistryProvider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function IssueView() {
|
|
45
|
+
const store = useStore(issueStoreOptions('abc123'))
|
|
46
|
+
const [issue] = store.useQuery(queryDb(tables.issue.select()))
|
|
47
|
+
return <div>{issue?.title}</div>
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
52
|
+
|
|
53
|
+
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
54
|
+
|
|
55
|
+
// Event definitions
|
|
56
|
+
export const events = {
|
|
57
|
+
issueCreated: Events.synced({
|
|
58
|
+
name: 'v1.IssueCreated',
|
|
59
|
+
schema: Schema.Struct({
|
|
60
|
+
id: Schema.String,
|
|
61
|
+
title: Schema.String,
|
|
62
|
+
status: Schema.Literal('todo', 'done'),
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
issueStatusChanged: Events.synced({
|
|
66
|
+
name: 'v1.IssueStatusChanged',
|
|
67
|
+
schema: Schema.Struct({
|
|
68
|
+
id: Schema.String,
|
|
69
|
+
status: Schema.Literal('todo', 'done'),
|
|
70
|
+
}),
|
|
71
|
+
}),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// State definition
|
|
75
|
+
export const tables = {
|
|
76
|
+
issue: State.SQLite.table({
|
|
77
|
+
name: 'issue',
|
|
78
|
+
columns: {
|
|
79
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
80
|
+
title: State.SQLite.text(),
|
|
81
|
+
status: State.SQLite.text(),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const materializers = State.SQLite.materializers(events, {
|
|
87
|
+
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
88
|
+
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
92
|
+
|
|
93
|
+
export const schema = makeSchema({ events, state })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Setting Up
|
|
97
|
+
|
|
98
|
+
### 1. Configure the Store
|
|
99
|
+
|
|
100
|
+
Create a store configuration file that exports a custom hook wrapping [`useStore()`](#usestoreoptions):
|
|
101
|
+
|
|
102
|
+
## `reference/framework-integrations/react/store.ts`
|
|
103
|
+
|
|
104
|
+
```ts filename="reference/framework-integrations/react/store.ts"
|
|
22
105
|
|
|
23
106
|
const adapter = makeInMemoryAdapter()
|
|
24
107
|
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
108
|
+
export const useAppStore = () =>
|
|
109
|
+
useStore({
|
|
110
|
+
storeId: 'app-root',
|
|
111
|
+
schema,
|
|
112
|
+
adapter,
|
|
113
|
+
batchUpdates,
|
|
114
|
+
})
|
|
30
115
|
```
|
|
31
116
|
|
|
32
117
|
### `reference/framework-integrations/react/schema.ts`
|
|
@@ -67,46 +152,39 @@ const state = State.SQLite.makeState({ tables, materializers })
|
|
|
67
152
|
export const schema = makeSchema({ events, state })
|
|
68
153
|
```
|
|
69
154
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`LiveStoreProvider` accepts optional logging configuration:
|
|
73
|
-
|
|
74
|
-
```tsx
|
|
155
|
+
The [`useStore()`](#usestoreoptions) hook accepts store configuration options and returns a store instance. It suspends while the store is loading, so components using it need to be wrapped in a `Suspense` boundary.
|
|
75
156
|
|
|
76
|
-
|
|
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
|
-
```
|
|
157
|
+
### 2. Set Up the Registry
|
|
88
158
|
|
|
89
|
-
|
|
159
|
+
Create a [`StoreRegistry`](#new-storeregistryconfig) and provide it via [`<StoreRegistryProvider>`](#storeregistryprovider). Wrap in a `<Suspense>` to handle loading states and a `<ErrorBoundary>` to handle errors:
|
|
90
160
|
|
|
91
|
-
## `reference/framework-integrations/react/
|
|
161
|
+
## `reference/framework-integrations/react/provider.tsx`
|
|
92
162
|
|
|
93
|
-
```tsx filename="reference/framework-integrations/react/
|
|
163
|
+
```tsx filename="reference/framework-integrations/react/provider.tsx"
|
|
94
164
|
|
|
95
|
-
|
|
165
|
+
export const Root: FC<{ children: ReactNode }> = ({ children }) => {
|
|
166
|
+
const [storeRegistry] = useState(() => new StoreRegistry())
|
|
96
167
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
168
|
+
return (
|
|
169
|
+
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
170
|
+
<Suspense fallback={<div>Loading LiveStore...</div>}>
|
|
171
|
+
<StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
|
|
172
|
+
</Suspense>
|
|
173
|
+
</ErrorBoundary>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
100
176
|
```
|
|
101
177
|
|
|
102
|
-
###
|
|
178
|
+
### 3. Use the Store
|
|
179
|
+
|
|
180
|
+
Components can now access the store via your custom hook:
|
|
103
181
|
|
|
104
182
|
## `reference/framework-integrations/react/use-store.tsx`
|
|
105
183
|
|
|
106
184
|
```tsx filename="reference/framework-integrations/react/use-store.tsx"
|
|
107
185
|
|
|
108
186
|
export const MyComponent: FC = () => {
|
|
109
|
-
const
|
|
187
|
+
const store = useAppStore()
|
|
110
188
|
|
|
111
189
|
useEffect(() => {
|
|
112
190
|
store.commit(events.todoCreated({ id: '1', text: 'Hello, world!' }))
|
|
@@ -154,7 +232,24 @@ const state = State.SQLite.makeState({ tables, materializers })
|
|
|
154
232
|
export const schema = makeSchema({ events, state })
|
|
155
233
|
```
|
|
156
234
|
|
|
157
|
-
###
|
|
235
|
+
### `reference/framework-integrations/react/store.ts`
|
|
236
|
+
|
|
237
|
+
```ts filename="reference/framework-integrations/react/store.ts"
|
|
238
|
+
|
|
239
|
+
const adapter = makeInMemoryAdapter()
|
|
240
|
+
|
|
241
|
+
export const useAppStore = () =>
|
|
242
|
+
useStore({
|
|
243
|
+
storeId: 'app-root',
|
|
244
|
+
schema,
|
|
245
|
+
adapter,
|
|
246
|
+
batchUpdates,
|
|
247
|
+
})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Querying Data
|
|
251
|
+
|
|
252
|
+
Use [`store.useQuery()`](#storeusequeryqueryable) to subscribe to reactive queries:
|
|
158
253
|
|
|
159
254
|
## `reference/framework-integrations/react/use-query.tsx`
|
|
160
255
|
|
|
@@ -165,7 +260,7 @@ const query$ = queryDb(tables.todos.where({ completed: true }).orderBy('id', 'de
|
|
|
165
260
|
})
|
|
166
261
|
|
|
167
262
|
export const CompletedTodos: FC = () => {
|
|
168
|
-
const
|
|
263
|
+
const store = useAppStore()
|
|
169
264
|
const todos = store.useQuery(query$)
|
|
170
265
|
|
|
171
266
|
return (
|
|
@@ -216,14 +311,31 @@ const state = State.SQLite.makeState({ tables, materializers })
|
|
|
216
311
|
export const schema = makeSchema({ events, state })
|
|
217
312
|
```
|
|
218
313
|
|
|
219
|
-
###
|
|
314
|
+
### `reference/framework-integrations/react/store.ts`
|
|
315
|
+
|
|
316
|
+
```ts filename="reference/framework-integrations/react/store.ts"
|
|
317
|
+
|
|
318
|
+
const adapter = makeInMemoryAdapter()
|
|
319
|
+
|
|
320
|
+
export const useAppStore = () =>
|
|
321
|
+
useStore({
|
|
322
|
+
storeId: 'app-root',
|
|
323
|
+
schema,
|
|
324
|
+
adapter,
|
|
325
|
+
batchUpdates,
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Client Documents
|
|
330
|
+
|
|
331
|
+
Use [`store.useClientDocument()`](#storeuseclientdocumenttable-id-options) for client-specific state:
|
|
220
332
|
|
|
221
333
|
## `reference/framework-integrations/react/use-client-document.tsx`
|
|
222
334
|
|
|
223
335
|
```tsx filename="reference/framework-integrations/react/use-client-document.tsx"
|
|
224
336
|
|
|
225
337
|
export const TodoItem: FC<{ id: string }> = ({ id }) => {
|
|
226
|
-
const
|
|
338
|
+
const store = useAppStore()
|
|
227
339
|
const [todo, updateTodo] = store.useClientDocument(tables.uiState, id)
|
|
228
340
|
|
|
229
341
|
return (
|
|
@@ -272,182 +384,31 @@ const state = State.SQLite.makeState({ tables, materializers })
|
|
|
272
384
|
export const schema = makeSchema({ events, state })
|
|
273
385
|
```
|
|
274
386
|
|
|
275
|
-
|
|
387
|
+
### `reference/framework-integrations/react/store.ts`
|
|
276
388
|
|
|
277
|
-
|
|
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:
|
|
389
|
+
```ts filename="reference/framework-integrations/react/store.ts"
|
|
356
390
|
|
|
357
|
-
|
|
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"
|
|
391
|
+
const adapter = makeInMemoryAdapter()
|
|
363
392
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
storeId:
|
|
393
|
+
export const useAppStore = () =>
|
|
394
|
+
useStore({
|
|
395
|
+
storeId: 'app-root',
|
|
367
396
|
schema,
|
|
368
|
-
adapter
|
|
397
|
+
adapter,
|
|
398
|
+
batchUpdates,
|
|
369
399
|
})
|
|
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
400
|
```
|
|
431
401
|
|
|
432
|
-
|
|
433
|
-
The Multi-Store API is still early in its development.
|
|
402
|
+
## Advanced Patterns
|
|
434
403
|
|
|
435
|
-
|
|
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
|
|
404
|
+
### Multiple Stores
|
|
445
405
|
|
|
446
|
-
|
|
406
|
+
You can have multiple stores within a single React application. This is useful for:
|
|
447
407
|
|
|
448
|
-
|
|
408
|
+
- **Partial data synchronization** - Load only the data you need, when you need it
|
|
409
|
+
- **Multi-tenant applications** - Separate stores for each workspace, organization, or team (like Slack workspaces or Linear teams)
|
|
449
410
|
|
|
450
|
-
|
|
411
|
+
Use the [`storeOptions()`](#storeoptionsoptions) helper for type-safe, reusable configurations, and then create multiple instances for the same store configuration by using different `storeId` values:
|
|
451
412
|
|
|
452
413
|
## `reference/framework-integrations/react/multi-store/store.ts`
|
|
453
414
|
|
|
@@ -508,23 +469,6 @@ const state = State.SQLite.makeState({ tables, materializers })
|
|
|
508
469
|
export const schema = makeSchema({ events, state })
|
|
509
470
|
```
|
|
510
471
|
|
|
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
472
|
## `reference/framework-integrations/react/multi-store/IssueView.tsx`
|
|
529
473
|
|
|
530
474
|
```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
|
|
@@ -618,113 +562,11 @@ export const issueStoreOptions = (issueId: string) =>
|
|
|
618
562
|
})
|
|
619
563
|
```
|
|
620
564
|
|
|
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
565
|
Each store instance is completely isolated with its own data, event log, and synchronization state.
|
|
724
566
|
|
|
725
|
-
### Preloading
|
|
567
|
+
### Preloading Stores
|
|
726
568
|
|
|
727
|
-
When you know a store will be needed soon,
|
|
569
|
+
When you know a store will be needed soon, preload it in advance to warm up the cache:
|
|
728
570
|
|
|
729
571
|
## `reference/framework-integrations/react/multi-store/PreloadedIssue.tsx`
|
|
730
572
|
|
|
@@ -734,9 +576,12 @@ export function PreloadedIssue({ issueId }: { issueId: string }) {
|
|
|
734
576
|
const [showIssue, setShowIssue] = useState(false)
|
|
735
577
|
const storeRegistry = useStoreRegistry()
|
|
736
578
|
|
|
737
|
-
// Preload the store when user hovers (before they click)
|
|
579
|
+
// Preload the store when the user hovers (before they click)
|
|
738
580
|
const handleMouseEnter = () => {
|
|
739
|
-
storeRegistry.preload(
|
|
581
|
+
storeRegistry.preload({
|
|
582
|
+
...issueStoreOptions(issueId),
|
|
583
|
+
unusedCacheTime: 10_000, // Optionally override options
|
|
584
|
+
})
|
|
740
585
|
}
|
|
741
586
|
|
|
742
587
|
return (
|
|
@@ -850,69 +695,243 @@ export const issueStoreOptions = (issueId: string) =>
|
|
|
850
695
|
})
|
|
851
696
|
```
|
|
852
697
|
|
|
853
|
-
This warms up the cache so the store is ready when the user navigates to it.
|
|
854
|
-
|
|
855
698
|
### StoreId Guidelines
|
|
856
699
|
|
|
857
700
|
When creating `storeId` values:
|
|
858
701
|
|
|
859
|
-
- **
|
|
702
|
+
- **Valid characters** - Only alphanumeric characters, underscores (`_`), and hyphens (`-`) are allowed (regex: `/^[a-zA-Z0-9_-]+$/`)
|
|
860
703
|
- **Globally unique** - Prefer globally unique IDs (e.g., nanoid) to prevent collisions
|
|
704
|
+
- **Use namespaces** - Prefix with the entity type (e.g., `workspace-abc123`, `issue-456`) to avoid collisions and easier identification when debugging
|
|
861
705
|
- **Keep them stable** - The same entity should always use the same `storeId` across renders
|
|
862
|
-
- **Sanitize user input** - If incorporating user data,
|
|
863
|
-
- **Document your conventions** - Document
|
|
706
|
+
- **Sanitize user input** - If incorporating user data, validate/sanitize to prevent injection attacks
|
|
707
|
+
- **Document your conventions** - Document special IDs like `user-current` as they're part of your API contract
|
|
708
|
+
|
|
709
|
+
### Logging
|
|
710
|
+
|
|
711
|
+
You can customize the logger and log level for debugging:
|
|
712
|
+
|
|
713
|
+
## `reference/framework-integrations/react/store-with-logging.ts`
|
|
714
|
+
|
|
715
|
+
```ts filename="reference/framework-integrations/react/store-with-logging.ts"
|
|
716
|
+
|
|
717
|
+
const adapter = makeInMemoryAdapter()
|
|
718
|
+
|
|
719
|
+
// ---cut---
|
|
720
|
+
export const useAppStore = () =>
|
|
721
|
+
useStore({
|
|
722
|
+
storeId: 'app-root',
|
|
723
|
+
schema,
|
|
724
|
+
adapter,
|
|
725
|
+
batchUpdates,
|
|
726
|
+
// Optional: swap the logger implementation
|
|
727
|
+
logger: Logger.prettyWithThread('app'),
|
|
728
|
+
// Optional: set minimum log level (use LogLevel.None to disable)
|
|
729
|
+
logLevel: LogLevel.Info,
|
|
730
|
+
})
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### `reference/framework-integrations/react/schema.ts`
|
|
734
|
+
|
|
735
|
+
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
736
|
+
|
|
737
|
+
export const tables = {
|
|
738
|
+
todos: State.SQLite.table({
|
|
739
|
+
name: 'todos',
|
|
740
|
+
columns: {
|
|
741
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
742
|
+
text: State.SQLite.text(),
|
|
743
|
+
completed: State.SQLite.boolean({ default: false }),
|
|
744
|
+
},
|
|
745
|
+
}),
|
|
746
|
+
uiState: State.SQLite.clientDocument({
|
|
747
|
+
name: 'UiState',
|
|
748
|
+
schema: Schema.Struct({ text: Schema.String }),
|
|
749
|
+
default: { value: { text: '' } },
|
|
750
|
+
}),
|
|
751
|
+
} as const
|
|
752
|
+
|
|
753
|
+
export const events = {
|
|
754
|
+
todoCreated: Events.synced({
|
|
755
|
+
name: 'v1.TodoCreated',
|
|
756
|
+
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
757
|
+
}),
|
|
758
|
+
} as const
|
|
759
|
+
|
|
760
|
+
const materializers = State.SQLite.materializers(events, {
|
|
761
|
+
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
762
|
+
tables.todos.insert({ id, text, completed: false }),
|
|
763
|
+
),
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
767
|
+
|
|
768
|
+
export const schema = makeSchema({ events, state })
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Use `LogLevel.None` to disable logging entirely.
|
|
772
|
+
|
|
773
|
+
## API Reference
|
|
864
774
|
|
|
865
|
-
###
|
|
775
|
+
### `storeOptions(options)`
|
|
866
776
|
|
|
867
|
-
|
|
777
|
+
Helper for defining reusable store options with full type inference. Returns options that can be passed to [`useStore()`](#usestoreoptions) or [`storeRegistry.preload()`](#storeregistrypreloadoptions).
|
|
868
778
|
|
|
869
|
-
|
|
779
|
+
Options:
|
|
780
|
+
- `storeId` - Unique identifier for this store instance
|
|
781
|
+
- `schema` - The LiveStore schema
|
|
782
|
+
- `adapter` - The platform adapter
|
|
783
|
+
- `unusedCacheTime?` - Time in ms to keep unused stores in cache (default: `60_000` in browser, `Infinity` in non-browser environments)
|
|
784
|
+
- `batchUpdates?` - Function for batching React updates (recommended)
|
|
785
|
+
- `boot?` - Function called when the store is loaded
|
|
786
|
+
- `onBootStatus?` - Callback for boot status updates
|
|
787
|
+
- `context?` - User-defined context for dependency injection
|
|
788
|
+
- `syncPayload?` - Payload sent to sync backend (e.g., auth tokens)
|
|
789
|
+
- `syncPayloadSchema?` - Schema for type-safe sync payload validation
|
|
790
|
+
- `confirmUnsavedChanges?` - Register beforeunload handler (default: `true`, web only)
|
|
791
|
+
- `logger?` - Custom logger implementation
|
|
792
|
+
- `logLevel?` - Log level (e.g., `LogLevel.Info`, `LogLevel.Debug`, `LogLevel.None`)
|
|
793
|
+
- `otelOptions?` - OpenTelemetry configuration (`{ tracer, rootSpanContext }`)
|
|
794
|
+
- `disableDevtools?` - Whether to disable devtools (`boolean | 'auto'`, default: `'auto'`)
|
|
795
|
+
- `debug?` - Debug options (`{ instanceId?: string }`)
|
|
796
|
+
|
|
797
|
+
### `useStore(options)`
|
|
798
|
+
|
|
799
|
+
Returns a store instance augmented with hooks ([`store.useQuery()`](#storeusequeryqueryable) and [`store.useClientDocument()`](#storeuseclientdocumenttable-id-options)) for reactive queries.
|
|
800
|
+
|
|
801
|
+
- Suspends until the store is loaded.
|
|
802
|
+
- Throws an error if loading fails.
|
|
803
|
+
- Store gets cached by its `storeId` in the [`StoreRegistry`](#new-storeregistryconfig). Multiple calls with the same `storeId` return the same store instance.
|
|
804
|
+
- Store is cached as long as it's being used, and after `unusedCacheTime` expires (default `60_000` ms in browser, `Infinity` in non-browser)
|
|
805
|
+
- Default store options can be configured in [`StoreRegistry`](#new-storeregistryconfig) constructor.
|
|
806
|
+
- Store options are only applied when the store is loaded. Subsequent calls with different options will not affect the store if it's already loaded and cached in the registry.
|
|
807
|
+
|
|
808
|
+
### `store.commit(...events)` / `store.commit(txnFn)`
|
|
809
|
+
|
|
810
|
+
Commits events to the store. Supports multiple calling patterns:
|
|
811
|
+
|
|
812
|
+
- `store.commit(...events)` - Commit one or more events
|
|
813
|
+
- `store.commit(txnFn)` - Commit events via a transaction function
|
|
814
|
+
- `store.commit(options, ...events)` - Commit with options
|
|
815
|
+
- `store.commit(options, txnFn)` - Options with transaction function
|
|
870
816
|
|
|
871
817
|
Options:
|
|
872
|
-
- `
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
-
|
|
818
|
+
- `skipRefresh` - Skip refreshing reactive queries after commit (advanced)
|
|
819
|
+
|
|
820
|
+
### `store.useQuery(queryable)`
|
|
821
|
+
|
|
822
|
+
Subscribes to a reactive query. Re-renders the component when the result changes.
|
|
823
|
+
|
|
824
|
+
- Takes any `Queryable`: `QueryBuilder`, `LiveQueryDef`, `SignalDef`, or `LiveQuery` instance
|
|
825
|
+
- Returns the query result
|
|
826
|
+
|
|
827
|
+
### `store.useClientDocument(table, id?, options?)`
|
|
828
|
+
|
|
829
|
+
React.useState-like hook for client-document tables.
|
|
830
|
+
|
|
831
|
+
- Returns `[row, setRow, id, query$]` tuple
|
|
832
|
+
- Works only with tables defined via `State.SQLite.clientDocument()`
|
|
833
|
+
- If the table has a default id, the `id` argument is optional
|
|
879
834
|
|
|
880
|
-
|
|
835
|
+
### `new StoreRegistry(config?)`
|
|
881
836
|
|
|
882
|
-
Creates a registry that
|
|
837
|
+
Creates a registry that coordinates store loading, caching, and retention.
|
|
883
838
|
|
|
884
839
|
Config:
|
|
885
|
-
- `defaultOptions
|
|
840
|
+
- `defaultOptions?` - Default options that are applied to all stores when they are loaded.:
|
|
841
|
+
- `batchUpdates?` - Function for batching React updates
|
|
842
|
+
- `unusedCacheTime?` - Cache time for unused stores
|
|
843
|
+
- `disableDevtools?` - Whether to disable devtools
|
|
844
|
+
- `confirmUnsavedChanges?` - beforeunload confirmation
|
|
845
|
+
- `otelOptions?` - OpenTelemetry configuration
|
|
846
|
+
- `debug?` - Debug options
|
|
847
|
+
- `runtime?` - Effect runtime for registry operations
|
|
886
848
|
|
|
887
|
-
|
|
849
|
+
### `<StoreRegistryProvider>`
|
|
888
850
|
|
|
889
|
-
React context provider that
|
|
851
|
+
React context provider that makes a [`StoreRegistry`](#new-storeregistryconfig) available to descendant components.
|
|
890
852
|
|
|
891
853
|
Props:
|
|
892
|
-
- `storeRegistry` - The registry instance
|
|
893
|
-
|
|
854
|
+
- `storeRegistry` - The registry instance
|
|
855
|
+
|
|
856
|
+
### `useStoreRegistry(override?)`
|
|
857
|
+
|
|
858
|
+
Hook that returns the [`StoreRegistry`](#new-storeregistryconfig) provided by the nearest [`<StoreRegistryProvider>`](#storeregistryprovider) ancestor, or the `override` if provided.
|
|
859
|
+
|
|
860
|
+
### `storeRegistry.preload(options)`
|
|
861
|
+
|
|
862
|
+
Loads a store (without suspending) to warm up the cache. Returns a Promise that resolves when loading completes. This is a fire-and-forget operation useful for warming up the cache.
|
|
863
|
+
|
|
864
|
+
## Framework-Specific Notes
|
|
865
|
+
|
|
866
|
+
### Vite
|
|
867
|
+
|
|
868
|
+
LiveStore works with Vite out of the box.
|
|
869
|
+
|
|
870
|
+
### Tanstack Start
|
|
871
|
+
|
|
872
|
+
LiveStore works with Tanstack Start out of the box.
|
|
873
|
+
|
|
874
|
+
#### Provider Placement
|
|
875
|
+
|
|
876
|
+
When using LiveStore with TanStack Start, place [`<StoreRegistryProvider>`](#storeregistryprovider) in the correct location to avoid remounting on navigation.
|
|
877
|
+
|
|
878
|
+
:::caution
|
|
879
|
+
**Do NOT place `<StoreRegistryProvider>` inside `shellComponent`**. The `shellComponent` can be re-rendered on navigation, causing LiveStore to remount and show the loading screen on every page transition.
|
|
880
|
+
:::
|
|
881
|
+
|
|
882
|
+
Use the `component` prop on `createRootRoute` for `<StoreRegistryProvider>`:
|
|
883
|
+
|
|
884
|
+
```tsx
|
|
894
885
|
|
|
895
|
-
|
|
886
|
+
export const Route = createRootRoute({
|
|
887
|
+
shellComponent: RootShell, // HTML structure only - NO state or providers
|
|
888
|
+
component: RootComponent, // App shell - StoreRegistryProvider goes HERE
|
|
889
|
+
})
|
|
896
890
|
|
|
897
|
-
|
|
891
|
+
// HTML document shell - keep this stateless
|
|
892
|
+
function RootShell({ children }: { children: React.ReactNode }) {
|
|
893
|
+
return (
|
|
894
|
+
<html lang="en">
|
|
895
|
+
<head><HeadContent /></head>
|
|
896
|
+
<body>
|
|
897
|
+
{children}
|
|
898
|
+
<Scripts />
|
|
899
|
+
</body>
|
|
900
|
+
</html>
|
|
901
|
+
)
|
|
902
|
+
}
|
|
898
903
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
904
|
+
// App shell - persists across SPA navigation
|
|
905
|
+
function RootComponent() {
|
|
906
|
+
const [storeRegistry] = useState(() => new StoreRegistry())
|
|
902
907
|
|
|
903
|
-
|
|
908
|
+
return (
|
|
909
|
+
<Suspense fallback={<div>Loading LiveStore...</div>}>
|
|
910
|
+
<StoreRegistryProvider storeRegistry={storeRegistry}>
|
|
911
|
+
<Outlet />
|
|
912
|
+
</StoreRegistryProvider>
|
|
913
|
+
</Suspense>
|
|
914
|
+
)
|
|
915
|
+
}
|
|
916
|
+
```
|
|
904
917
|
|
|
905
|
-
|
|
918
|
+
TanStack Start's `shellComponent` is designed for SSR HTML streaming and may be re-evaluated on server requests during navigation. When `<StoreRegistryProvider>` is placed there, the WebSocket connection is re-established and all LiveStore state is re-initialized on each navigation.
|
|
906
919
|
|
|
907
|
-
|
|
920
|
+
If you see the loading screen on every navigation, check your server logs for multiple "Launching WebSocket" messages.
|
|
908
921
|
|
|
909
|
-
|
|
922
|
+
### Expo / React Native
|
|
923
|
+
|
|
924
|
+
LiveStore has a first-class integration with Expo / React Native via `@livestore/adapter-expo`. See the [Expo Adapter documentation](/platform-adapters/expo-adapter).
|
|
925
|
+
|
|
926
|
+
### Next.js
|
|
927
|
+
|
|
928
|
+
Given various Next.js limitations, LiveStore doesn't yet work with Next.js out of the box.
|
|
910
929
|
|
|
911
930
|
### Complete Example
|
|
912
931
|
|
|
913
932
|
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
933
|
|
|
915
|
-
## Technical
|
|
934
|
+
## Technical Notes
|
|
916
935
|
|
|
917
|
-
- `@livestore/react` uses `React.useState` under the hood for `useQuery` / `useClientDocument` to bind LiveStore's reactivity to React's reactivity. Some libraries
|
|
936
|
+
- `@livestore/react` uses `React.useState()` under the hood for `useQuery()` / `useClientDocument()` to bind LiveStore's reactivity to React's reactivity. Some libraries use `React.useSyncExternalStore()` for similar purposes but `React.useState()` is more efficient for LiveStore's architecture.
|
|
918
937
|
- `@livestore/react` supports React Strict Mode.
|