@livestore/livestore 0.4.0-dev.22 → 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 +14 -7
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +0 -15
- 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/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 +1 -1
- package/dist/mod.d.ts.map +1 -1
- 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 +30 -5
- package/dist/store/StoreRegistry.d.ts.map +1 -1
- package/dist/store/StoreRegistry.js +54 -31
- package/dist/store/StoreRegistry.js.map +1 -1
- package/dist/store/StoreRegistry.test.js +251 -250
- package/dist/store/StoreRegistry.test.js.map +1 -1
- package/dist/store/create-store.d.ts +6 -2
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +13 -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 +3 -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 +70 -5
- 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 +81 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +128 -45
- 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 +58 -17
- 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 +17 -26
- 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 +1 -0
- package/src/reactive.test.ts +3 -2
- package/src/reactive.ts +22 -23
- package/src/store/StoreRegistry.test.ts +317 -293
- package/src/store/StoreRegistry.ts +63 -38
- package/src/store/create-store.ts +26 -11
- package/src/store/devtools.ts +5 -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 +47 -8
- package/src/store/store.ts +172 -55
- 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 -3
- package/docs/building-with-livestore/crud/index.md +0 -3
- package/docs/building-with-livestore/data-modeling/index.md +0 -30
- 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/todo-workspaces/index.md +0 -885
- package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
- package/docs/building-with-livestore/opentelemetry/index.md +0 -227
- 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 -94
- 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 -625
- 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 -937
- 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 -882
- package/docs/getting-started/node/index.md +0 -115
- package/docs/getting-started/react-web/index.md +0 -626
- package/docs/getting-started/solid/index.md +0 -3
- package/docs/getting-started/vue/index.md +0 -471
- package/docs/index.md +0 -208
- package/docs/llms.txt +0 -146
- 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 -413
- 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 -111
- 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 -377
- package/docs/patterns/effect/index.md +0 -1505
- 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 -209
- 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 -262
- 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 -287
- 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 -530
- 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,937 +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
|
-
- Supports multiple store instances
|
|
11
|
-
- Transactional state transitions (via `batchUpdates`)
|
|
12
|
-
- Also supports Expo / React Native via `@livestore/adapter-expo`
|
|
13
|
-
|
|
14
|
-
## Core Concepts
|
|
15
|
-
|
|
16
|
-
When using LiveStore in React, you'll primarily interact with these fundamental components:
|
|
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
|
|
21
|
-
|
|
22
|
-
Stores are cached by their `storeId` and automatically disposed after being unused for a configurable duration (`unusedCacheTime`).
|
|
23
|
-
|
|
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"
|
|
105
|
-
|
|
106
|
-
const adapter = makeInMemoryAdapter()
|
|
107
|
-
|
|
108
|
-
export const useAppStore = () =>
|
|
109
|
-
useStore({
|
|
110
|
-
storeId: 'app-root',
|
|
111
|
-
schema,
|
|
112
|
-
adapter,
|
|
113
|
-
batchUpdates,
|
|
114
|
-
})
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
118
|
-
|
|
119
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
120
|
-
|
|
121
|
-
export const tables = {
|
|
122
|
-
todos: State.SQLite.table({
|
|
123
|
-
name: 'todos',
|
|
124
|
-
columns: {
|
|
125
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
126
|
-
text: State.SQLite.text(),
|
|
127
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
128
|
-
},
|
|
129
|
-
}),
|
|
130
|
-
uiState: State.SQLite.clientDocument({
|
|
131
|
-
name: 'UiState',
|
|
132
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
133
|
-
default: { value: { text: '' } },
|
|
134
|
-
}),
|
|
135
|
-
} as const
|
|
136
|
-
|
|
137
|
-
export const events = {
|
|
138
|
-
todoCreated: Events.synced({
|
|
139
|
-
name: 'v1.TodoCreated',
|
|
140
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
141
|
-
}),
|
|
142
|
-
} as const
|
|
143
|
-
|
|
144
|
-
const materializers = State.SQLite.materializers(events, {
|
|
145
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
146
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
147
|
-
),
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
151
|
-
|
|
152
|
-
export const schema = makeSchema({ events, state })
|
|
153
|
-
```
|
|
154
|
-
|
|
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.
|
|
156
|
-
|
|
157
|
-
### 2. Set Up the Registry
|
|
158
|
-
|
|
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:
|
|
160
|
-
|
|
161
|
-
## `reference/framework-integrations/react/provider.tsx`
|
|
162
|
-
|
|
163
|
-
```tsx filename="reference/framework-integrations/react/provider.tsx"
|
|
164
|
-
|
|
165
|
-
export const Root: FC<{ children: ReactNode }> = ({ children }) => {
|
|
166
|
-
const [storeRegistry] = useState(() => new StoreRegistry())
|
|
167
|
-
|
|
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
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 3. Use the Store
|
|
179
|
-
|
|
180
|
-
Components can now access the store via your custom hook:
|
|
181
|
-
|
|
182
|
-
## `reference/framework-integrations/react/use-store.tsx`
|
|
183
|
-
|
|
184
|
-
```tsx filename="reference/framework-integrations/react/use-store.tsx"
|
|
185
|
-
|
|
186
|
-
export const MyComponent: FC = () => {
|
|
187
|
-
const store = useAppStore()
|
|
188
|
-
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
store.commit(events.todoCreated({ id: '1', text: 'Hello, world!' }))
|
|
191
|
-
}, [store])
|
|
192
|
-
|
|
193
|
-
return <div>...</div>
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
198
|
-
|
|
199
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
200
|
-
|
|
201
|
-
export const tables = {
|
|
202
|
-
todos: State.SQLite.table({
|
|
203
|
-
name: 'todos',
|
|
204
|
-
columns: {
|
|
205
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
206
|
-
text: State.SQLite.text(),
|
|
207
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
208
|
-
},
|
|
209
|
-
}),
|
|
210
|
-
uiState: State.SQLite.clientDocument({
|
|
211
|
-
name: 'UiState',
|
|
212
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
213
|
-
default: { value: { text: '' } },
|
|
214
|
-
}),
|
|
215
|
-
} as const
|
|
216
|
-
|
|
217
|
-
export const events = {
|
|
218
|
-
todoCreated: Events.synced({
|
|
219
|
-
name: 'v1.TodoCreated',
|
|
220
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
221
|
-
}),
|
|
222
|
-
} as const
|
|
223
|
-
|
|
224
|
-
const materializers = State.SQLite.materializers(events, {
|
|
225
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
226
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
227
|
-
),
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
231
|
-
|
|
232
|
-
export const schema = makeSchema({ events, state })
|
|
233
|
-
```
|
|
234
|
-
|
|
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:
|
|
253
|
-
|
|
254
|
-
## `reference/framework-integrations/react/use-query.tsx`
|
|
255
|
-
|
|
256
|
-
```tsx filename="reference/framework-integrations/react/use-query.tsx"
|
|
257
|
-
|
|
258
|
-
const query$ = queryDb(tables.todos.where({ completed: true }).orderBy('id', 'desc'), {
|
|
259
|
-
label: 'completedTodos',
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
export const CompletedTodos: FC = () => {
|
|
263
|
-
const store = useAppStore()
|
|
264
|
-
const todos = store.useQuery(query$)
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<div>
|
|
268
|
-
{todos.map((todo) => (
|
|
269
|
-
<div key={todo.id}>{todo.text}</div>
|
|
270
|
-
))}
|
|
271
|
-
</div>
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
277
|
-
|
|
278
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
279
|
-
|
|
280
|
-
export const tables = {
|
|
281
|
-
todos: State.SQLite.table({
|
|
282
|
-
name: 'todos',
|
|
283
|
-
columns: {
|
|
284
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
285
|
-
text: State.SQLite.text(),
|
|
286
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
287
|
-
},
|
|
288
|
-
}),
|
|
289
|
-
uiState: State.SQLite.clientDocument({
|
|
290
|
-
name: 'UiState',
|
|
291
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
292
|
-
default: { value: { text: '' } },
|
|
293
|
-
}),
|
|
294
|
-
} as const
|
|
295
|
-
|
|
296
|
-
export const events = {
|
|
297
|
-
todoCreated: Events.synced({
|
|
298
|
-
name: 'v1.TodoCreated',
|
|
299
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
300
|
-
}),
|
|
301
|
-
} as const
|
|
302
|
-
|
|
303
|
-
const materializers = State.SQLite.materializers(events, {
|
|
304
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
305
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
306
|
-
),
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
310
|
-
|
|
311
|
-
export const schema = makeSchema({ events, state })
|
|
312
|
-
```
|
|
313
|
-
|
|
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:
|
|
332
|
-
|
|
333
|
-
## `reference/framework-integrations/react/use-client-document.tsx`
|
|
334
|
-
|
|
335
|
-
```tsx filename="reference/framework-integrations/react/use-client-document.tsx"
|
|
336
|
-
|
|
337
|
-
export const TodoItem: FC<{ id: string }> = ({ id }) => {
|
|
338
|
-
const store = useAppStore()
|
|
339
|
-
const [todo, updateTodo] = store.useClientDocument(tables.uiState, id)
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<button type="button" onClick={() => updateTodo({ text: 'Hello, world!' })}>
|
|
343
|
-
{todo.text}
|
|
344
|
-
</button>
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
### `reference/framework-integrations/react/schema.ts`
|
|
350
|
-
|
|
351
|
-
```ts filename="reference/framework-integrations/react/schema.ts"
|
|
352
|
-
|
|
353
|
-
export const tables = {
|
|
354
|
-
todos: State.SQLite.table({
|
|
355
|
-
name: 'todos',
|
|
356
|
-
columns: {
|
|
357
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
358
|
-
text: State.SQLite.text(),
|
|
359
|
-
completed: State.SQLite.boolean({ default: false }),
|
|
360
|
-
},
|
|
361
|
-
}),
|
|
362
|
-
uiState: State.SQLite.clientDocument({
|
|
363
|
-
name: 'UiState',
|
|
364
|
-
schema: Schema.Struct({ text: Schema.String }),
|
|
365
|
-
default: { value: { text: '' } },
|
|
366
|
-
}),
|
|
367
|
-
} as const
|
|
368
|
-
|
|
369
|
-
export const events = {
|
|
370
|
-
todoCreated: Events.synced({
|
|
371
|
-
name: 'v1.TodoCreated',
|
|
372
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
|
|
373
|
-
}),
|
|
374
|
-
} as const
|
|
375
|
-
|
|
376
|
-
const materializers = State.SQLite.materializers(events, {
|
|
377
|
-
[events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
|
|
378
|
-
tables.todos.insert({ id, text, completed: false }),
|
|
379
|
-
),
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
383
|
-
|
|
384
|
-
export const schema = makeSchema({ events, state })
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### `reference/framework-integrations/react/store.ts`
|
|
388
|
-
|
|
389
|
-
```ts filename="reference/framework-integrations/react/store.ts"
|
|
390
|
-
|
|
391
|
-
const adapter = makeInMemoryAdapter()
|
|
392
|
-
|
|
393
|
-
export const useAppStore = () =>
|
|
394
|
-
useStore({
|
|
395
|
-
storeId: 'app-root',
|
|
396
|
-
schema,
|
|
397
|
-
adapter,
|
|
398
|
-
batchUpdates,
|
|
399
|
-
})
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
## Advanced Patterns
|
|
403
|
-
|
|
404
|
-
### Multiple Stores
|
|
405
|
-
|
|
406
|
-
You can have multiple stores within a single React application. This is useful for:
|
|
407
|
-
|
|
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)
|
|
410
|
-
|
|
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:
|
|
412
|
-
|
|
413
|
-
## `reference/framework-integrations/react/multi-store/store.ts`
|
|
414
|
-
|
|
415
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
416
|
-
|
|
417
|
-
// Define reusable store configuration with storeOptions()
|
|
418
|
-
// This helper provides type safety and can be reused across your app
|
|
419
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
420
|
-
storeOptions({
|
|
421
|
-
storeId: `issue-${issueId}`,
|
|
422
|
-
schema,
|
|
423
|
-
adapter: makeInMemoryAdapter(),
|
|
424
|
-
})
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
428
|
-
|
|
429
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
430
|
-
|
|
431
|
-
// Event definitions
|
|
432
|
-
export const events = {
|
|
433
|
-
issueCreated: Events.synced({
|
|
434
|
-
name: 'v1.IssueCreated',
|
|
435
|
-
schema: Schema.Struct({
|
|
436
|
-
id: Schema.String,
|
|
437
|
-
title: Schema.String,
|
|
438
|
-
status: Schema.Literal('todo', 'done'),
|
|
439
|
-
}),
|
|
440
|
-
}),
|
|
441
|
-
issueStatusChanged: Events.synced({
|
|
442
|
-
name: 'v1.IssueStatusChanged',
|
|
443
|
-
schema: Schema.Struct({
|
|
444
|
-
id: Schema.String,
|
|
445
|
-
status: Schema.Literal('todo', 'done'),
|
|
446
|
-
}),
|
|
447
|
-
}),
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// State definition
|
|
451
|
-
export const tables = {
|
|
452
|
-
issue: State.SQLite.table({
|
|
453
|
-
name: 'issue',
|
|
454
|
-
columns: {
|
|
455
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
456
|
-
title: State.SQLite.text(),
|
|
457
|
-
status: State.SQLite.text(),
|
|
458
|
-
},
|
|
459
|
-
}),
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const materializers = State.SQLite.materializers(events, {
|
|
463
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
464
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
468
|
-
|
|
469
|
-
export const schema = makeSchema({ events, state })
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
## `reference/framework-integrations/react/multi-store/IssueView.tsx`
|
|
473
|
-
|
|
474
|
-
```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
|
|
475
|
-
|
|
476
|
-
export function IssueView({ issueId }: { issueId: string }) {
|
|
477
|
-
// useStore() suspends the component until the store is loaded
|
|
478
|
-
// If the same store was already loaded, it returns immediately
|
|
479
|
-
const issueStore = useStore(issueStoreOptions(issueId))
|
|
480
|
-
|
|
481
|
-
// Query data from the store
|
|
482
|
-
const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
|
|
483
|
-
|
|
484
|
-
if (!issue) return <div>Issue not found</div>
|
|
485
|
-
|
|
486
|
-
return (
|
|
487
|
-
<div>
|
|
488
|
-
<h3>{issue.title}</h3>
|
|
489
|
-
<p>Status: {issue.status}</p>
|
|
490
|
-
</div>
|
|
491
|
-
)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Wrap with Suspense and ErrorBoundary for loading and error states
|
|
495
|
-
export function IssueViewWithSuspense({ issueId }: { issueId: string }) {
|
|
496
|
-
return (
|
|
497
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
498
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
499
|
-
<IssueView issueId={issueId} />
|
|
500
|
-
</Suspense>
|
|
501
|
-
</ErrorBoundary>
|
|
502
|
-
)
|
|
503
|
-
}
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
507
|
-
|
|
508
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
509
|
-
|
|
510
|
-
// Event definitions
|
|
511
|
-
export const events = {
|
|
512
|
-
issueCreated: Events.synced({
|
|
513
|
-
name: 'v1.IssueCreated',
|
|
514
|
-
schema: Schema.Struct({
|
|
515
|
-
id: Schema.String,
|
|
516
|
-
title: Schema.String,
|
|
517
|
-
status: Schema.Literal('todo', 'done'),
|
|
518
|
-
}),
|
|
519
|
-
}),
|
|
520
|
-
issueStatusChanged: Events.synced({
|
|
521
|
-
name: 'v1.IssueStatusChanged',
|
|
522
|
-
schema: Schema.Struct({
|
|
523
|
-
id: Schema.String,
|
|
524
|
-
status: Schema.Literal('todo', 'done'),
|
|
525
|
-
}),
|
|
526
|
-
}),
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// State definition
|
|
530
|
-
export const tables = {
|
|
531
|
-
issue: State.SQLite.table({
|
|
532
|
-
name: 'issue',
|
|
533
|
-
columns: {
|
|
534
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
535
|
-
title: State.SQLite.text(),
|
|
536
|
-
status: State.SQLite.text(),
|
|
537
|
-
},
|
|
538
|
-
}),
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const materializers = State.SQLite.materializers(events, {
|
|
542
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
543
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
547
|
-
|
|
548
|
-
export const schema = makeSchema({ events, state })
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### `reference/framework-integrations/react/multi-store/store.ts`
|
|
552
|
-
|
|
553
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
554
|
-
|
|
555
|
-
// Define reusable store configuration with storeOptions()
|
|
556
|
-
// This helper provides type safety and can be reused across your app
|
|
557
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
558
|
-
storeOptions({
|
|
559
|
-
storeId: `issue-${issueId}`,
|
|
560
|
-
schema,
|
|
561
|
-
adapter: makeInMemoryAdapter(),
|
|
562
|
-
})
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
Each store instance is completely isolated with its own data, event log, and synchronization state.
|
|
566
|
-
|
|
567
|
-
### Preloading Stores
|
|
568
|
-
|
|
569
|
-
When you know a store will be needed soon, preload it in advance to warm up the cache:
|
|
570
|
-
|
|
571
|
-
## `reference/framework-integrations/react/multi-store/PreloadedIssue.tsx`
|
|
572
|
-
|
|
573
|
-
```tsx filename="reference/framework-integrations/react/multi-store/PreloadedIssue.tsx"
|
|
574
|
-
|
|
575
|
-
export function PreloadedIssue({ issueId }: { issueId: string }) {
|
|
576
|
-
const [showIssue, setShowIssue] = useState(false)
|
|
577
|
-
const storeRegistry = useStoreRegistry()
|
|
578
|
-
|
|
579
|
-
// Preload the store when the user hovers (before they click)
|
|
580
|
-
const handleMouseEnter = () => {
|
|
581
|
-
storeRegistry.preload({
|
|
582
|
-
...issueStoreOptions(issueId),
|
|
583
|
-
unusedCacheTime: 10_000, // Optionally override options
|
|
584
|
-
})
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return (
|
|
588
|
-
<div>
|
|
589
|
-
{!showIssue ? (
|
|
590
|
-
<button type="button" onMouseEnter={handleMouseEnter} onClick={() => setShowIssue(true)}>
|
|
591
|
-
Show Issue
|
|
592
|
-
</button>
|
|
593
|
-
) : (
|
|
594
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
595
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
596
|
-
<IssueView issueId={issueId} />
|
|
597
|
-
</Suspense>
|
|
598
|
-
</ErrorBoundary>
|
|
599
|
-
)}
|
|
600
|
-
</div>
|
|
601
|
-
)
|
|
602
|
-
}
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
### `reference/framework-integrations/react/multi-store/IssueView.tsx`
|
|
606
|
-
|
|
607
|
-
```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
|
|
608
|
-
|
|
609
|
-
export function IssueView({ issueId }: { issueId: string }) {
|
|
610
|
-
// useStore() suspends the component until the store is loaded
|
|
611
|
-
// If the same store was already loaded, it returns immediately
|
|
612
|
-
const issueStore = useStore(issueStoreOptions(issueId))
|
|
613
|
-
|
|
614
|
-
// Query data from the store
|
|
615
|
-
const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
|
|
616
|
-
|
|
617
|
-
if (!issue) return <div>Issue not found</div>
|
|
618
|
-
|
|
619
|
-
return (
|
|
620
|
-
<div>
|
|
621
|
-
<h3>{issue.title}</h3>
|
|
622
|
-
<p>Status: {issue.status}</p>
|
|
623
|
-
</div>
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Wrap with Suspense and ErrorBoundary for loading and error states
|
|
628
|
-
export function IssueViewWithSuspense({ issueId }: { issueId: string }) {
|
|
629
|
-
return (
|
|
630
|
-
<ErrorBoundary fallback={<div>Error loading issue</div>}>
|
|
631
|
-
<Suspense fallback={<div>Loading issue...</div>}>
|
|
632
|
-
<IssueView issueId={issueId} />
|
|
633
|
-
</Suspense>
|
|
634
|
-
</ErrorBoundary>
|
|
635
|
-
)
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
### `reference/framework-integrations/react/multi-store/schema.ts`
|
|
640
|
-
|
|
641
|
-
```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
|
|
642
|
-
|
|
643
|
-
// Event definitions
|
|
644
|
-
export const events = {
|
|
645
|
-
issueCreated: Events.synced({
|
|
646
|
-
name: 'v1.IssueCreated',
|
|
647
|
-
schema: Schema.Struct({
|
|
648
|
-
id: Schema.String,
|
|
649
|
-
title: Schema.String,
|
|
650
|
-
status: Schema.Literal('todo', 'done'),
|
|
651
|
-
}),
|
|
652
|
-
}),
|
|
653
|
-
issueStatusChanged: Events.synced({
|
|
654
|
-
name: 'v1.IssueStatusChanged',
|
|
655
|
-
schema: Schema.Struct({
|
|
656
|
-
id: Schema.String,
|
|
657
|
-
status: Schema.Literal('todo', 'done'),
|
|
658
|
-
}),
|
|
659
|
-
}),
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// State definition
|
|
663
|
-
export const tables = {
|
|
664
|
-
issue: State.SQLite.table({
|
|
665
|
-
name: 'issue',
|
|
666
|
-
columns: {
|
|
667
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
668
|
-
title: State.SQLite.text(),
|
|
669
|
-
status: State.SQLite.text(),
|
|
670
|
-
},
|
|
671
|
-
}),
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const materializers = State.SQLite.materializers(events, {
|
|
675
|
-
'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
|
|
676
|
-
'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
680
|
-
|
|
681
|
-
export const schema = makeSchema({ events, state })
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### `reference/framework-integrations/react/multi-store/store.ts`
|
|
685
|
-
|
|
686
|
-
```ts filename="reference/framework-integrations/react/multi-store/store.ts"
|
|
687
|
-
|
|
688
|
-
// Define reusable store configuration with storeOptions()
|
|
689
|
-
// This helper provides type safety and can be reused across your app
|
|
690
|
-
export const issueStoreOptions = (issueId: string) =>
|
|
691
|
-
storeOptions({
|
|
692
|
-
storeId: `issue-${issueId}`,
|
|
693
|
-
schema,
|
|
694
|
-
adapter: makeInMemoryAdapter(),
|
|
695
|
-
})
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
### StoreId Guidelines
|
|
699
|
-
|
|
700
|
-
When creating `storeId` values:
|
|
701
|
-
|
|
702
|
-
- **Valid characters** - Only alphanumeric characters, underscores (`_`), and hyphens (`-`) are allowed (regex: `/^[a-zA-Z0-9_-]+$/`)
|
|
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
|
|
705
|
-
- **Keep them stable** - The same entity should always use the same `storeId` across renders
|
|
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
|
|
774
|
-
|
|
775
|
-
### `storeOptions(options)`
|
|
776
|
-
|
|
777
|
-
Helper for defining reusable store options with full type inference. Returns options that can be passed to [`useStore()`](#usestoreoptions) or [`storeRegistry.preload()`](#storeregistrypreloadoptions).
|
|
778
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
Options:
|
|
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
|
|
834
|
-
|
|
835
|
-
### `new StoreRegistry(config?)`
|
|
836
|
-
|
|
837
|
-
Creates a registry that coordinates store loading, caching, and retention.
|
|
838
|
-
|
|
839
|
-
Config:
|
|
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
|
|
848
|
-
|
|
849
|
-
### `<StoreRegistryProvider>`
|
|
850
|
-
|
|
851
|
-
React context provider that makes a [`StoreRegistry`](#new-storeregistryconfig) available to descendant components.
|
|
852
|
-
|
|
853
|
-
Props:
|
|
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
|
|
885
|
-
|
|
886
|
-
export const Route = createRootRoute({
|
|
887
|
-
shellComponent: RootShell, // HTML structure only - NO state or providers
|
|
888
|
-
component: RootComponent, // App shell - StoreRegistryProvider goes HERE
|
|
889
|
-
})
|
|
890
|
-
|
|
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
|
-
}
|
|
903
|
-
|
|
904
|
-
// App shell - persists across SPA navigation
|
|
905
|
-
function RootComponent() {
|
|
906
|
-
const [storeRegistry] = useState(() => new StoreRegistry())
|
|
907
|
-
|
|
908
|
-
return (
|
|
909
|
-
<Suspense fallback={<div>Loading LiveStore...</div>}>
|
|
910
|
-
<StoreRegistryProvider storeRegistry={storeRegistry}>
|
|
911
|
-
<Outlet />
|
|
912
|
-
</StoreRegistryProvider>
|
|
913
|
-
</Suspense>
|
|
914
|
-
)
|
|
915
|
-
}
|
|
916
|
-
```
|
|
917
|
-
|
|
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.
|
|
919
|
-
|
|
920
|
-
If you see the loading screen on every navigation, check your server logs for multiple "Launching WebSocket" messages.
|
|
921
|
-
|
|
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.
|
|
929
|
-
|
|
930
|
-
### Complete Example
|
|
931
|
-
|
|
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.
|
|
933
|
-
|
|
934
|
-
## Technical Notes
|
|
935
|
-
|
|
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.
|
|
937
|
-
- `@livestore/react` supports React Strict Mode.
|