@livestore/react 0.4.0-dev.20 → 0.4.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/LiveStoreContext.d.ts +27 -0
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +18 -0
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +9 -2
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +2 -1
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +60 -16
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.js +125 -216
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.test.js +224 -307
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -1
- package/dist/experimental/multi-store/types.d.ts +4 -23
- package/dist/experimental/multi-store/types.d.ts.map +1 -1
- package/dist/experimental/multi-store/useStore.d.ts +1 -1
- package/dist/experimental/multi-store/useStore.d.ts.map +1 -1
- package/dist/experimental/multi-store/useStore.js +5 -10
- package/dist/experimental/multi-store/useStore.js.map +1 -1
- package/dist/experimental/multi-store/useStore.test.js +95 -41
- package/dist/experimental/multi-store/useStore.test.js.map +1 -1
- package/dist/useClientDocument.d.ts +33 -0
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useStore.d.ts +51 -0
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +51 -0
- package/dist/useStore.js.map +1 -1
- package/package.json +6 -6
- package/src/LiveStoreContext.ts +27 -0
- package/src/LiveStoreProvider.tsx +9 -0
- package/src/experimental/multi-store/StoreRegistry.test.ts +236 -349
- package/src/experimental/multi-store/StoreRegistry.ts +171 -265
- package/src/experimental/multi-store/types.ts +31 -49
- package/src/experimental/multi-store/useStore.test.tsx +120 -48
- package/src/experimental/multi-store/useStore.ts +5 -13
- package/src/useClientDocument.ts +35 -0
- package/src/useStore.ts +51 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
2
|
import type { Store } from '@livestore/livestore'
|
|
3
3
|
import { StoreInternalsSymbol } from '@livestore/livestore'
|
|
4
|
-
import {
|
|
4
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
5
|
+
import { act, type RenderHookResult, type RenderResult, render, renderHook, waitFor } from '@testing-library/react'
|
|
5
6
|
import * as React from 'react'
|
|
6
|
-
import {
|
|
7
|
+
import { describe, expect, it } from 'vitest'
|
|
7
8
|
import { schema } from '../../__tests__/fixture.tsx'
|
|
8
9
|
import { StoreRegistry } from './StoreRegistry.ts'
|
|
9
10
|
import { StoreRegistryProvider } from './StoreRegistryContext.tsx'
|
|
@@ -12,31 +13,48 @@ import type { CachedStoreOptions } from './types.ts'
|
|
|
12
13
|
import { useStore } from './useStore.ts'
|
|
13
14
|
|
|
14
15
|
describe('experimental useStore', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
it('should return the same promise instance for concurrent getOrLoadStore calls', async () => {
|
|
17
|
+
const registry = new StoreRegistry()
|
|
18
|
+
const options = testStoreOptions()
|
|
19
|
+
|
|
20
|
+
// Make two concurrent calls during loading
|
|
21
|
+
const firstStore = registry.getOrLoadPromise(options)
|
|
22
|
+
const secondStore = registry.getOrLoadPromise(options)
|
|
23
|
+
|
|
24
|
+
// Both should be promises (store is loading)
|
|
25
|
+
expect(firstStore).toBeInstanceOf(Promise)
|
|
26
|
+
expect(secondStore).toBeInstanceOf(Promise)
|
|
27
|
+
|
|
28
|
+
// EXPECTED BEHAVIOR: Same promise instance for React.use() compatibility
|
|
29
|
+
// ACTUAL BEHAVIOR: Different promise instances (Effect.runPromise creates new wrapper)
|
|
30
|
+
expect(firstStore).toBe(secondStore)
|
|
31
|
+
|
|
32
|
+
// Cleanup
|
|
33
|
+
await firstStore
|
|
34
|
+
await cleanupAfterUnmount(() => {})
|
|
18
35
|
})
|
|
19
36
|
|
|
20
|
-
it('
|
|
37
|
+
it('works with Suspense boundary', async () => {
|
|
21
38
|
const registry = new StoreRegistry()
|
|
22
39
|
const options = testStoreOptions()
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
let view: RenderResult | undefined
|
|
42
|
+
await act(async () => {
|
|
43
|
+
view = render(
|
|
44
|
+
<StoreRegistryProvider storeRegistry={registry}>
|
|
45
|
+
<React.Suspense fallback={<div data-testid="fallback" />}>
|
|
46
|
+
<StoreConsumer options={options} />
|
|
47
|
+
</React.Suspense>
|
|
48
|
+
</StoreRegistryProvider>,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
const renderedView = view ?? shouldNeverHappen('render failed')
|
|
34
52
|
|
|
35
|
-
//
|
|
36
|
-
await waitForSuspenseResolved(
|
|
37
|
-
expect(
|
|
53
|
+
// After loading completes, should show the actual content
|
|
54
|
+
await waitForSuspenseResolved(renderedView)
|
|
55
|
+
expect(renderedView.getByTestId('ready')).toBeDefined()
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
await cleanupAfterUnmount(() => renderedView.unmount())
|
|
40
58
|
})
|
|
41
59
|
|
|
42
60
|
it('does not re-suspend on subsequent renders when store is already loaded', async () => {
|
|
@@ -51,20 +69,26 @@ describe('experimental useStore', () => {
|
|
|
51
69
|
</StoreRegistryProvider>
|
|
52
70
|
)
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
let view: RenderResult | undefined
|
|
73
|
+
await act(async () => {
|
|
74
|
+
view = render(<Wrapper opts={options} />)
|
|
75
|
+
})
|
|
76
|
+
const renderedView = view ?? shouldNeverHappen('render failed')
|
|
55
77
|
|
|
56
78
|
// Wait for initial load
|
|
57
|
-
await waitForSuspenseResolved(
|
|
58
|
-
expect(
|
|
79
|
+
await waitForSuspenseResolved(renderedView)
|
|
80
|
+
expect(renderedView.getByTestId('ready')).toBeDefined()
|
|
59
81
|
|
|
60
82
|
// Rerender with new options object (but same storeId)
|
|
61
|
-
|
|
83
|
+
await act(async () => {
|
|
84
|
+
renderedView.rerender(<Wrapper opts={{ ...options }} />)
|
|
85
|
+
})
|
|
62
86
|
|
|
63
87
|
// Should not show fallback
|
|
64
|
-
expect(
|
|
65
|
-
expect(
|
|
88
|
+
expect(renderedView.queryByTestId('fallback')).toBeNull()
|
|
89
|
+
expect(renderedView.getByTestId('ready')).toBeDefined()
|
|
66
90
|
|
|
67
|
-
|
|
91
|
+
await cleanupAfterUnmount(() => renderedView.unmount())
|
|
68
92
|
})
|
|
69
93
|
|
|
70
94
|
it('throws when store loading fails', async () => {
|
|
@@ -74,8 +98,8 @@ describe('experimental useStore', () => {
|
|
|
74
98
|
adapter: null,
|
|
75
99
|
})
|
|
76
100
|
|
|
77
|
-
// Pre-load the store to cache the error
|
|
78
|
-
|
|
101
|
+
// Pre-load the store to cache the error (error happens synchronously)
|
|
102
|
+
expect(() => registry.getOrLoadPromise(badOptions)).toThrow()
|
|
79
103
|
|
|
80
104
|
// Now when useStore tries to get it, it should throw synchronously
|
|
81
105
|
expect(() =>
|
|
@@ -92,16 +116,20 @@ describe('experimental useStore', () => {
|
|
|
92
116
|
const registry = new StoreRegistry()
|
|
93
117
|
const options = testStoreOptions()
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
let hook: RenderHookResult<Store<typeof schema>, CachedStoreOptions<typeof schema>> | undefined
|
|
120
|
+
await act(async () => {
|
|
121
|
+
hook = renderHook(() => useStore(options), {
|
|
122
|
+
wrapper: makeProvider(registry, { suspense: true }),
|
|
123
|
+
reactStrictMode: strictMode,
|
|
124
|
+
})
|
|
98
125
|
})
|
|
126
|
+
const { result, unmount } = hook ?? shouldNeverHappen('renderHook failed')
|
|
99
127
|
|
|
100
128
|
// Wait for store to be ready
|
|
101
129
|
await waitForStoreReady(result)
|
|
102
130
|
expect(result.current[StoreInternalsSymbol].clientSession).toBeDefined()
|
|
103
131
|
|
|
104
|
-
|
|
132
|
+
await cleanupAfterUnmount(unmount)
|
|
105
133
|
})
|
|
106
134
|
|
|
107
135
|
it('handles switching between different storeId values', async () => {
|
|
@@ -110,10 +138,14 @@ describe('experimental useStore', () => {
|
|
|
110
138
|
const optionsA = testStoreOptions({ storeId: 'store-a' })
|
|
111
139
|
const optionsB = testStoreOptions({ storeId: 'store-b' })
|
|
112
140
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
let hook: RenderHookResult<Store<typeof schema>, CachedStoreOptions<typeof schema>> | undefined
|
|
142
|
+
await act(async () => {
|
|
143
|
+
hook = renderHook((opts) => useStore(opts), {
|
|
144
|
+
initialProps: optionsA,
|
|
145
|
+
wrapper: makeProvider(registry, { suspense: true }),
|
|
146
|
+
})
|
|
116
147
|
})
|
|
148
|
+
const { result, rerender, unmount } = hook ?? shouldNeverHappen('renderHook failed')
|
|
117
149
|
|
|
118
150
|
// Wait for first store to load
|
|
119
151
|
await waitForStoreReady(result)
|
|
@@ -121,7 +153,9 @@ describe('experimental useStore', () => {
|
|
|
121
153
|
expect(storeA[StoreInternalsSymbol].clientSession).toBeDefined()
|
|
122
154
|
|
|
123
155
|
// Switch to different storeId
|
|
124
|
-
|
|
156
|
+
await act(async () => {
|
|
157
|
+
rerender(optionsB)
|
|
158
|
+
})
|
|
125
159
|
|
|
126
160
|
// Wait for second store to load and verify it's different from the first
|
|
127
161
|
await waitFor(() => {
|
|
@@ -133,7 +167,47 @@ describe('experimental useStore', () => {
|
|
|
133
167
|
expect(storeB[StoreInternalsSymbol].clientSession).toBeDefined()
|
|
134
168
|
expect(storeB).not.toBe(storeA)
|
|
135
169
|
|
|
136
|
-
|
|
170
|
+
await cleanupAfterUnmount(unmount)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// useStore doesn't handle unusedCacheTime=0 correctly because retain is called in useEffect (after render)
|
|
174
|
+
// See https://github.com/livestorejs/livestore/issues/916
|
|
175
|
+
it.skip('should load store with unusedCacheTime set to 0', async () => {
|
|
176
|
+
const registry = new StoreRegistry({ defaultOptions: { unusedCacheTime: 0 } })
|
|
177
|
+
const options = testStoreOptions({ unusedCacheTime: 0 })
|
|
178
|
+
|
|
179
|
+
const StoreConsumerWithVerification = ({ opts }: { opts: CachedStoreOptions<typeof schema> }) => {
|
|
180
|
+
const store = useStore(opts)
|
|
181
|
+
// Verify store is usable - access internals to confirm it's not disposed
|
|
182
|
+
const clientSession = store[StoreInternalsSymbol].clientSession
|
|
183
|
+
return <div data-testid="ready" data-has-session={String(clientSession !== undefined)} />
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let view: RenderResult | undefined
|
|
187
|
+
await act(async () => {
|
|
188
|
+
view = render(
|
|
189
|
+
<StoreRegistryProvider storeRegistry={registry}>
|
|
190
|
+
<React.Suspense fallback={<div data-testid="fallback" />}>
|
|
191
|
+
<StoreConsumerWithVerification opts={options} />
|
|
192
|
+
</React.Suspense>
|
|
193
|
+
</StoreRegistryProvider>,
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
const renderedView = view ?? shouldNeverHappen('render failed')
|
|
197
|
+
|
|
198
|
+
await waitForSuspenseResolved(renderedView)
|
|
199
|
+
|
|
200
|
+
// Store should be usable while component is mounted
|
|
201
|
+
const readyElement = renderedView.getByTestId('ready')
|
|
202
|
+
expect(readyElement.getAttribute('data-has-session')).toBe('true')
|
|
203
|
+
|
|
204
|
+
// Allow some time to pass to ensure store isn't prematurely disposed
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
206
|
+
|
|
207
|
+
// Store should still be usable after waiting
|
|
208
|
+
expect(readyElement.getAttribute('data-has-session')).toBe('true')
|
|
209
|
+
|
|
210
|
+
await cleanupAfterUnmount(() => renderedView.unmount())
|
|
137
211
|
})
|
|
138
212
|
})
|
|
139
213
|
|
|
@@ -154,28 +228,26 @@ const makeProvider =
|
|
|
154
228
|
return content
|
|
155
229
|
}
|
|
156
230
|
|
|
231
|
+
let testStoreCounter = 0
|
|
232
|
+
|
|
157
233
|
const testStoreOptions = (overrides: Partial<CachedStoreOptions<typeof schema>> = {}) =>
|
|
158
234
|
storeOptions({
|
|
159
|
-
storeId:
|
|
235
|
+
storeId: overrides.storeId ?? `test-store-${testStoreCounter++}`,
|
|
160
236
|
schema,
|
|
161
237
|
adapter: makeInMemoryAdapter(),
|
|
162
238
|
...overrides,
|
|
163
239
|
})
|
|
164
240
|
|
|
165
241
|
/**
|
|
166
|
-
* Cleans up after component unmount
|
|
242
|
+
* Cleans up after component unmount and waits for pending operations to settle.
|
|
167
243
|
*
|
|
168
244
|
* When components using stores unmount, the StoreRegistry schedules garbage collection
|
|
169
|
-
* timers for inactive stores.
|
|
170
|
-
* subsequent tests, causing cross-test pollution and flaky failures.
|
|
171
|
-
*
|
|
172
|
-
* This helper switches to fake timers, executes only the already-pending timers
|
|
173
|
-
* (allowing stores to shut down cleanly), then restores real timers for the next test.
|
|
245
|
+
* timers for inactive stores. This helper waits for those timers to complete naturally.
|
|
174
246
|
*/
|
|
175
|
-
const
|
|
176
|
-
vi.useFakeTimers()
|
|
247
|
+
const cleanupAfterUnmount = async (cleanup: () => void): Promise<void> => {
|
|
177
248
|
cleanup()
|
|
178
|
-
|
|
249
|
+
// Allow any pending microtasks/timers to settle
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
179
251
|
}
|
|
180
252
|
|
|
181
253
|
/**
|
|
@@ -7,7 +7,7 @@ import { useStoreRegistry } from './StoreRegistryContext.tsx'
|
|
|
7
7
|
import type { CachedStoreOptions } from './types.ts'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Suspense
|
|
10
|
+
* Suspense and Error Boundary friendly hook.
|
|
11
11
|
* - Returns data or throws (Promise|Error).
|
|
12
12
|
* - No loading or error states are returned.
|
|
13
13
|
*/
|
|
@@ -16,19 +16,11 @@ export const useStore = <TSchema extends LiveStoreSchema>(
|
|
|
16
16
|
): Store<TSchema> & ReactApi => {
|
|
17
17
|
const storeRegistry = useStoreRegistry()
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
(onChange: () => void) => storeRegistry.subscribe(options.storeId, onChange),
|
|
21
|
-
[storeRegistry, options.storeId],
|
|
22
|
-
)
|
|
23
|
-
const getSnapshot = React.useCallback(() => {
|
|
24
|
-
const storeOrPromise = storeRegistry.getOrLoad(options)
|
|
19
|
+
React.useEffect(() => storeRegistry.retain(options), [storeRegistry, options])
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
const storeOrPromise = React.useMemo(() => storeRegistry.getOrLoadPromise(options), [storeRegistry, options])
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
}, [storeRegistry, options])
|
|
23
|
+
const store = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return withReactApi(loadedStore)
|
|
25
|
+
return withReactApi(store)
|
|
34
26
|
}
|
package/src/useClientDocument.ts
CHANGED
|
@@ -9,6 +9,17 @@ import React from 'react'
|
|
|
9
9
|
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
10
10
|
import { useQueryRef } from './useQuery.ts'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Return type of `useClientDocument` hook.
|
|
14
|
+
*
|
|
15
|
+
* A tuple providing React-style state access to a client-document table row:
|
|
16
|
+
* - `[0]` row: The current value (decoded according to the table schema)
|
|
17
|
+
* - `[1]` setRow: Setter function to update the document
|
|
18
|
+
* - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
|
|
19
|
+
* - `[3]` query$: The underlying `LiveQuery` for advanced use cases
|
|
20
|
+
*
|
|
21
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
22
|
+
*/
|
|
12
23
|
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
13
24
|
row: TTableDef['Value'],
|
|
14
25
|
setRow: StateSetters<TTableDef>,
|
|
@@ -140,10 +151,34 @@ export const useClientDocument: {
|
|
|
140
151
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
141
152
|
}
|
|
142
153
|
|
|
154
|
+
/**
|
|
155
|
+
* A function that dispatches an action. Mirrors React's `Dispatch` type.
|
|
156
|
+
* @typeParam A - The action type
|
|
157
|
+
*/
|
|
143
158
|
export type Dispatch<A> = (action: A) => void
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A state update that can be either a partial value or a function returning a partial value.
|
|
162
|
+
* Used when the client-document table has `partialSet: true`.
|
|
163
|
+
* @typeParam S - The state type
|
|
164
|
+
*/
|
|
144
165
|
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A state update that can be either a full value or a function returning a full value.
|
|
169
|
+
* Mirrors React's `SetStateAction` type.
|
|
170
|
+
* @typeParam S - The state type
|
|
171
|
+
*/
|
|
145
172
|
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
146
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The setter function type for `useClientDocument`, determined by the table's `partialSet` option.
|
|
176
|
+
*
|
|
177
|
+
* - If `partialSet: false` (default), requires full state replacement
|
|
178
|
+
* - If `partialSet: true`, accepts partial updates merged with existing state
|
|
179
|
+
*
|
|
180
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
181
|
+
*/
|
|
147
182
|
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
148
183
|
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
149
184
|
? SetStateAction<TTableDef['Value']>
|
package/src/useStore.ts
CHANGED
|
@@ -7,6 +7,19 @@ import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
|
7
7
|
import { useClientDocument } from './useClientDocument.ts'
|
|
8
8
|
import { useQuery } from './useQuery.ts'
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Augments a Store instance with React-specific methods (`useQuery`, `useClientDocument`).
|
|
12
|
+
*
|
|
13
|
+
* This is called automatically by `useStore()` and `LiveStoreProvider`. You typically
|
|
14
|
+
* don't need to call it directly unless you're building custom integrations.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Usually not needed—useStore() does this automatically
|
|
19
|
+
* const store = withReactApi(myStore)
|
|
20
|
+
* const todos = store.useQuery(tables.todos.all())
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
10
23
|
export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSchema>): Store<TSchema> & ReactApi => {
|
|
11
24
|
// @ts-expect-error TODO properly implement this
|
|
12
25
|
|
|
@@ -17,6 +30,44 @@ export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSche
|
|
|
17
30
|
return store as Store<TSchema> & ReactApi
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Returns the current Store instance from React context, augmented with React-specific methods.
|
|
35
|
+
*
|
|
36
|
+
* Use this hook when you need direct access to the Store for operations like
|
|
37
|
+
* `store.commit()`, `store.subscribe()`, or accessing `store.sessionId`.
|
|
38
|
+
*
|
|
39
|
+
* For reactive queries, prefer `useQuery()` or `useClientDocument()` which handle
|
|
40
|
+
* subscriptions and re-renders automatically.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const MyComponent = () => {
|
|
45
|
+
* const { store } = useStore()
|
|
46
|
+
*
|
|
47
|
+
* const handleClick = () => {
|
|
48
|
+
* store.commit(events.todoCreated({ id: nanoid(), text: 'New todo' }))
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* return <button onClick={handleClick}>Add Todo</button>
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Access store metadata
|
|
58
|
+
* const { store } = useStore()
|
|
59
|
+
* console.log('Session ID:', store.sessionId)
|
|
60
|
+
* console.log('Client ID:', store.clientId)
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Use with an explicit store instance (bypasses context)
|
|
66
|
+
* const { store } = useStore({ store: myExternalStore })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @throws Error if called outside of `<LiveStoreProvider>` or before the store is running
|
|
70
|
+
*/
|
|
20
71
|
export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {
|
|
21
72
|
if (options?.store !== undefined) {
|
|
22
73
|
return { store: withReactApi(options.store) }
|