@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.
Files changed (216) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +130 -2
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +185 -6
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/effect/mod.d.ts +1 -1
  20. package/dist/effect/mod.d.ts.map +1 -1
  21. package/dist/effect/mod.js +3 -1
  22. package/dist/effect/mod.js.map +1 -1
  23. package/dist/live-queries/base-class.d.ts +3 -3
  24. package/dist/live-queries/base-class.d.ts.map +1 -1
  25. package/dist/live-queries/base-class.js +2 -2
  26. package/dist/live-queries/base-class.js.map +1 -1
  27. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  28. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  29. package/dist/live-queries/client-document-get-query.js +1 -1
  30. package/dist/live-queries/client-document-get-query.js.map +1 -1
  31. package/dist/live-queries/computed.d.ts.map +1 -1
  32. package/dist/live-queries/computed.js +2 -2
  33. package/dist/live-queries/computed.js.map +1 -1
  34. package/dist/live-queries/db-query.js +14 -14
  35. package/dist/live-queries/db-query.js.map +1 -1
  36. package/dist/live-queries/db-query.test.js +2 -2
  37. package/dist/live-queries/db-query.test.js.map +1 -1
  38. package/dist/live-queries/signal.test.js +2 -2
  39. package/dist/live-queries/signal.test.js.map +1 -1
  40. package/dist/mod.d.ts +2 -1
  41. package/dist/mod.d.ts.map +1 -1
  42. package/dist/mod.js +1 -0
  43. package/dist/mod.js.map +1 -1
  44. package/dist/reactive.d.ts +9 -9
  45. package/dist/reactive.d.ts.map +1 -1
  46. package/dist/reactive.js +9 -26
  47. package/dist/reactive.js.map +1 -1
  48. package/dist/reactive.test.js +2 -2
  49. package/dist/reactive.test.js.map +1 -1
  50. package/dist/store/StoreRegistry.d.ts +215 -0
  51. package/dist/store/StoreRegistry.d.ts.map +1 -0
  52. package/dist/store/StoreRegistry.js +267 -0
  53. package/dist/store/StoreRegistry.js.map +1 -0
  54. package/dist/store/StoreRegistry.test.d.ts +2 -0
  55. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  56. package/dist/store/StoreRegistry.test.js +381 -0
  57. package/dist/store/StoreRegistry.test.js.map +1 -0
  58. package/dist/store/create-store.d.ts +56 -6
  59. package/dist/store/create-store.d.ts.map +1 -1
  60. package/dist/store/create-store.js +32 -7
  61. package/dist/store/create-store.js.map +1 -1
  62. package/dist/store/devtools.d.ts +1 -1
  63. package/dist/store/devtools.d.ts.map +1 -1
  64. package/dist/store/devtools.js +16 -3
  65. package/dist/store/devtools.js.map +1 -1
  66. package/dist/store/store-eventstream.test.js +2 -2
  67. package/dist/store/store-eventstream.test.js.map +1 -1
  68. package/dist/store/store-types.d.ts +59 -9
  69. package/dist/store/store-types.d.ts.map +1 -1
  70. package/dist/store/store-types.js.map +1 -1
  71. package/dist/store/store-types.test.js +1 -1
  72. package/dist/store/store-types.test.js.map +1 -1
  73. package/dist/store/store.d.ts +102 -6
  74. package/dist/store/store.d.ts.map +1 -1
  75. package/dist/store/store.js +148 -47
  76. package/dist/store/store.js.map +1 -1
  77. package/dist/utils/dev.js.map +1 -1
  78. package/dist/utils/stack-info.js +2 -2
  79. package/dist/utils/stack-info.js.map +1 -1
  80. package/dist/utils/tests/fixture.d.ts +1 -1
  81. package/dist/utils/tests/fixture.d.ts.map +1 -1
  82. package/dist/utils/tests/fixture.js.map +1 -1
  83. package/dist/utils/tests/otel.d.ts.map +1 -1
  84. package/dist/utils/tests/otel.js +5 -5
  85. package/dist/utils/tests/otel.js.map +1 -1
  86. package/package.json +59 -18
  87. package/src/QueryCache.ts +1 -1
  88. package/src/SqliteDbWrapper.test.ts +4 -2
  89. package/src/SqliteDbWrapper.ts +12 -11
  90. package/src/ambient.d.ts +0 -7
  91. package/src/effect/LiveStore.test.ts +61 -0
  92. package/src/effect/LiveStore.ts +381 -8
  93. package/src/effect/mod.ts +13 -1
  94. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  95. package/src/live-queries/base-class.ts +7 -6
  96. package/src/live-queries/client-document-get-query.ts +4 -2
  97. package/src/live-queries/computed.ts +3 -2
  98. package/src/live-queries/db-query.test.ts +3 -2
  99. package/src/live-queries/db-query.ts +15 -15
  100. package/src/live-queries/signal.test.ts +3 -2
  101. package/src/mod.ts +2 -0
  102. package/src/reactive.test.ts +3 -2
  103. package/src/reactive.ts +22 -23
  104. package/src/store/StoreRegistry.test.ts +540 -0
  105. package/src/store/StoreRegistry.ts +418 -0
  106. package/src/store/create-store.ts +76 -15
  107. package/src/store/devtools.ts +20 -6
  108. package/src/store/store-eventstream.test.ts +4 -2
  109. package/src/store/store-types.test.ts +3 -1
  110. package/src/store/store-types.ts +64 -13
  111. package/src/store/store.ts +197 -60
  112. package/src/utils/dev.ts +2 -2
  113. package/src/utils/stack-info.ts +2 -2
  114. package/src/utils/tests/fixture.ts +2 -1
  115. package/src/utils/tests/otel.ts +8 -7
  116. package/docs/api/index.md +0 -3
  117. package/docs/building-with-livestore/complex-ui-state/index.md +0 -5
  118. package/docs/building-with-livestore/crud/index.md +0 -5
  119. package/docs/building-with-livestore/data-modeling/index.md +0 -1
  120. package/docs/building-with-livestore/debugging/index.md +0 -17
  121. package/docs/building-with-livestore/devtools/index.md +0 -79
  122. package/docs/building-with-livestore/events/index.md +0 -355
  123. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  124. package/docs/building-with-livestore/examples/index.md +0 -30
  125. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -891
  126. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  127. package/docs/building-with-livestore/opentelemetry/index.md +0 -208
  128. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  129. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  130. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  131. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  132. package/docs/building-with-livestore/state/sql-queries/index.md +0 -72
  133. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  134. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  135. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  136. package/docs/building-with-livestore/store/index.md +0 -281
  137. package/docs/building-with-livestore/syncing/index.md +0 -136
  138. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  139. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  140. package/docs/examples/cloudflare-adapter/index.md +0 -44
  141. package/docs/examples/expo-adapter/index.md +0 -44
  142. package/docs/examples/index.md +0 -55
  143. package/docs/examples/node-adapter/index.md +0 -44
  144. package/docs/examples/web-adapter/index.md +0 -52
  145. package/docs/framework-integrations/custom-elements/index.md +0 -142
  146. package/docs/framework-integrations/react-integration/index.md +0 -918
  147. package/docs/framework-integrations/solid-integration/index.md +0 -293
  148. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  149. package/docs/framework-integrations/vue-integration/index.md +0 -294
  150. package/docs/getting-started/expo/index.md +0 -736
  151. package/docs/getting-started/node/index.md +0 -115
  152. package/docs/getting-started/react-web/index.md +0 -573
  153. package/docs/getting-started/solid/index.md +0 -3
  154. package/docs/getting-started/vue/index.md +0 -471
  155. package/docs/index.md +0 -209
  156. package/docs/llms.txt +0 -147
  157. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  158. package/docs/misc/FAQ/index.md +0 -37
  159. package/docs/misc/community/index.md +0 -88
  160. package/docs/misc/credits/index.md +0 -14
  161. package/docs/misc/design-partners/index.md +0 -13
  162. package/docs/misc/package-management/index.md +0 -21
  163. package/docs/misc/performance/index.md +0 -25
  164. package/docs/misc/resources/index.md +0 -46
  165. package/docs/misc/state-of-the-project/index.md +0 -37
  166. package/docs/misc/troubleshooting/index.md +0 -82
  167. package/docs/overview/concepts/index.md +0 -78
  168. package/docs/overview/how-livestore-works/index.md +0 -56
  169. package/docs/overview/introduction/index.md +0 -5
  170. package/docs/overview/technology-comparison/index.md +0 -40
  171. package/docs/overview/when-livestore/index.md +0 -81
  172. package/docs/overview/why-livestore/index.md +0 -5
  173. package/docs/patterns/ai/index.md +0 -15
  174. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  175. package/docs/patterns/app-evolution/index.md +0 -72
  176. package/docs/patterns/auth/index.md +0 -226
  177. package/docs/patterns/effect/index.md +0 -1495
  178. package/docs/patterns/encryption/index.md +0 -6
  179. package/docs/patterns/external-data/index.md +0 -5
  180. package/docs/patterns/file-management/index.md +0 -11
  181. package/docs/patterns/file-structure/index.md +0 -14
  182. package/docs/patterns/list-ordering/index.md +0 -369
  183. package/docs/patterns/offline/index.md +0 -32
  184. package/docs/patterns/orm/index.md +0 -18
  185. package/docs/patterns/presence/index.md +0 -11
  186. package/docs/patterns/rich-text-editing/index.md +0 -11
  187. package/docs/patterns/server-side-clients/index.md +0 -97
  188. package/docs/patterns/side-effects/index.md +0 -11
  189. package/docs/patterns/state-machines/index.md +0 -11
  190. package/docs/patterns/storybook/index.md +0 -192
  191. package/docs/patterns/undo-redo/index.md +0 -9
  192. package/docs/patterns/version-control/index.md +0 -8
  193. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  194. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  195. package/docs/platform-adapters/expo-adapter/index.md +0 -245
  196. package/docs/platform-adapters/node-adapter/index.md +0 -160
  197. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  198. package/docs/platform-adapters/web-adapter/index.md +0 -218
  199. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  200. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  201. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  202. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  203. package/docs/sync-providers/cloudflare/index.md +0 -773
  204. package/docs/sync-providers/custom/index.md +0 -65
  205. package/docs/sync-providers/electricsql/index.md +0 -159
  206. package/docs/sync-providers/s2/index.md +0 -230
  207. package/docs/tutorial/0-welcome/index.md +0 -48
  208. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  209. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  210. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -511
  211. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  212. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  213. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  214. package/docs/tutorial/7-next-steps/index.md +0 -22
  215. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  216. 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.