@livestore/react 0.4.0-dev.20 → 0.4.0-dev.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/StoreRegistryContext.d.ts +56 -0
  3. package/dist/StoreRegistryContext.d.ts.map +1 -0
  4. package/dist/StoreRegistryContext.js +61 -0
  5. package/dist/StoreRegistryContext.js.map +1 -0
  6. package/dist/__tests__/fixture.d.ts.map +1 -1
  7. package/dist/__tests__/fixture.js +1 -6
  8. package/dist/__tests__/fixture.js.map +1 -1
  9. package/dist/experimental/components/LiveList.d.ts +4 -2
  10. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  11. package/dist/experimental/components/LiveList.js +6 -5
  12. package/dist/experimental/components/LiveList.js.map +1 -1
  13. package/dist/experimental/mod.d.ts +0 -1
  14. package/dist/experimental/mod.d.ts.map +1 -1
  15. package/dist/experimental/mod.js +0 -1
  16. package/dist/experimental/mod.js.map +1 -1
  17. package/dist/mod.d.ts +4 -3
  18. package/dist/mod.d.ts.map +1 -1
  19. package/dist/mod.js +3 -2
  20. package/dist/mod.js.map +1 -1
  21. package/dist/useClientDocument.d.ts +33 -0
  22. package/dist/useClientDocument.d.ts.map +1 -1
  23. package/dist/useClientDocument.js +1 -4
  24. package/dist/useClientDocument.js.map +1 -1
  25. package/dist/useQuery.d.ts +1 -1
  26. package/dist/useQuery.d.ts.map +1 -1
  27. package/dist/useQuery.js +2 -5
  28. package/dist/useQuery.js.map +1 -1
  29. package/dist/useStore.d.ts +62 -7
  30. package/dist/useStore.d.ts.map +1 -1
  31. package/dist/useStore.js +73 -15
  32. package/dist/useStore.js.map +1 -1
  33. package/dist/useStore.test.d.ts.map +1 -0
  34. package/dist/useStore.test.js +196 -0
  35. package/dist/useStore.test.js.map +1 -0
  36. package/package.json +7 -7
  37. package/src/StoreRegistryContext.tsx +69 -0
  38. package/src/__tests__/fixture.tsx +1 -13
  39. package/src/experimental/components/LiveList.tsx +13 -4
  40. package/src/experimental/mod.ts +0 -1
  41. package/src/mod.ts +4 -3
  42. package/src/useClientDocument.ts +36 -5
  43. package/src/useQuery.ts +2 -6
  44. package/src/useStore.test.tsx +271 -0
  45. package/src/useStore.ts +102 -23
  46. package/dist/LiveStoreContext.d.ts +0 -13
  47. package/dist/LiveStoreContext.d.ts.map +0 -1
  48. package/dist/LiveStoreContext.js +0 -3
  49. package/dist/LiveStoreContext.js.map +0 -1
  50. package/dist/LiveStoreProvider.d.ts +0 -66
  51. package/dist/LiveStoreProvider.d.ts.map +0 -1
  52. package/dist/LiveStoreProvider.js +0 -232
  53. package/dist/LiveStoreProvider.js.map +0 -1
  54. package/dist/LiveStoreProvider.test.d.ts +0 -2
  55. package/dist/LiveStoreProvider.test.d.ts.map +0 -1
  56. package/dist/LiveStoreProvider.test.js +0 -117
  57. package/dist/LiveStoreProvider.test.js.map +0 -1
  58. package/dist/experimental/multi-store/StoreRegistry.d.ts +0 -61
  59. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +0 -1
  60. package/dist/experimental/multi-store/StoreRegistry.js +0 -275
  61. package/dist/experimental/multi-store/StoreRegistry.js.map +0 -1
  62. package/dist/experimental/multi-store/StoreRegistry.test.d.ts +0 -2
  63. package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +0 -1
  64. package/dist/experimental/multi-store/StoreRegistry.test.js +0 -464
  65. package/dist/experimental/multi-store/StoreRegistry.test.js.map +0 -1
  66. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +0 -10
  67. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +0 -1
  68. package/dist/experimental/multi-store/StoreRegistryContext.js +0 -15
  69. package/dist/experimental/multi-store/StoreRegistryContext.js.map +0 -1
  70. package/dist/experimental/multi-store/mod.d.ts +0 -6
  71. package/dist/experimental/multi-store/mod.d.ts.map +0 -1
  72. package/dist/experimental/multi-store/mod.js +0 -6
  73. package/dist/experimental/multi-store/mod.js.map +0 -1
  74. package/dist/experimental/multi-store/storeOptions.d.ts +0 -4
  75. package/dist/experimental/multi-store/storeOptions.d.ts.map +0 -1
  76. package/dist/experimental/multi-store/storeOptions.js +0 -4
  77. package/dist/experimental/multi-store/storeOptions.js.map +0 -1
  78. package/dist/experimental/multi-store/types.d.ts +0 -44
  79. package/dist/experimental/multi-store/types.d.ts.map +0 -1
  80. package/dist/experimental/multi-store/types.js +0 -2
  81. package/dist/experimental/multi-store/types.js.map +0 -1
  82. package/dist/experimental/multi-store/useStore.d.ts +0 -11
  83. package/dist/experimental/multi-store/useStore.d.ts.map +0 -1
  84. package/dist/experimental/multi-store/useStore.js +0 -21
  85. package/dist/experimental/multi-store/useStore.js.map +0 -1
  86. package/dist/experimental/multi-store/useStore.test.d.ts.map +0 -1
  87. package/dist/experimental/multi-store/useStore.test.js +0 -144
  88. package/dist/experimental/multi-store/useStore.test.js.map +0 -1
  89. package/src/LiveStoreContext.ts +0 -14
  90. package/src/LiveStoreProvider.test.tsx +0 -248
  91. package/src/LiveStoreProvider.tsx +0 -421
  92. package/src/experimental/multi-store/StoreRegistry.test.ts +0 -631
  93. package/src/experimental/multi-store/StoreRegistry.ts +0 -347
  94. package/src/experimental/multi-store/StoreRegistryContext.tsx +0 -23
  95. package/src/experimental/multi-store/mod.ts +0 -5
  96. package/src/experimental/multi-store/storeOptions.ts +0 -8
  97. package/src/experimental/multi-store/types.ts +0 -55
  98. package/src/experimental/multi-store/useStore.test.tsx +0 -197
  99. package/src/experimental/multi-store/useStore.ts +0 -34
  100. /package/dist/{experimental/multi-store/useStore.test.d.ts → useStore.test.d.ts} +0 -0
@@ -0,0 +1,271 @@
1
+ import { makeInMemoryAdapter } from '@livestore/adapter-web'
2
+ import {
3
+ type RegistryStoreOptions,
4
+ type Store,
5
+ StoreInternalsSymbol,
6
+ StoreRegistry,
7
+ storeOptions,
8
+ } from '@livestore/livestore'
9
+ import { shouldNeverHappen } from '@livestore/utils'
10
+ import { act, type RenderHookResult, type RenderResult, render, renderHook, waitFor } from '@testing-library/react'
11
+ import * as React from 'react'
12
+ import { describe, expect, it } from 'vitest'
13
+ import { schema } from './__tests__/fixture.tsx'
14
+ import { StoreRegistryProvider } from './StoreRegistryContext.tsx'
15
+ import { useStore } from './useStore.ts'
16
+
17
+ describe('experimental useStore', () => {
18
+ it('should return the same promise instance for concurrent getOrLoadStore calls', async () => {
19
+ const storeRegistry = new StoreRegistry()
20
+ const options = testStoreOptions()
21
+
22
+ // Make two concurrent calls during loading
23
+ const firstStore = storeRegistry.getOrLoadPromise(options)
24
+ const secondStore = storeRegistry.getOrLoadPromise(options)
25
+
26
+ // Both should be promises (store is loading)
27
+ expect(firstStore).toBeInstanceOf(Promise)
28
+ expect(secondStore).toBeInstanceOf(Promise)
29
+
30
+ // EXPECTED BEHAVIOR: Same promise instance for React.use() compatibility
31
+ // ACTUAL BEHAVIOR: Different promise instances (Effect.runPromise creates new wrapper)
32
+ expect(firstStore).toBe(secondStore)
33
+
34
+ // Cleanup
35
+ await firstStore
36
+ await cleanupAfterUnmount(() => {})
37
+ })
38
+
39
+ it('works with Suspense boundary', async () => {
40
+ const storeRegistry = new StoreRegistry()
41
+ const options = testStoreOptions()
42
+
43
+ let view: RenderResult | undefined
44
+ await act(async () => {
45
+ view = render(
46
+ <StoreRegistryProvider storeRegistry={storeRegistry}>
47
+ <React.Suspense fallback={<div data-testid="fallback" />}>
48
+ <StoreConsumer options={options} />
49
+ </React.Suspense>
50
+ </StoreRegistryProvider>,
51
+ )
52
+ })
53
+ const renderedView = view ?? shouldNeverHappen('render failed')
54
+
55
+ // After loading completes, should show the actual content
56
+ await waitForSuspenseResolved(renderedView)
57
+ expect(renderedView.getByTestId('ready')).toBeDefined()
58
+
59
+ await cleanupAfterUnmount(() => renderedView.unmount())
60
+ })
61
+
62
+ it('does not re-suspend on subsequent renders when store is already loaded', async () => {
63
+ const storeRegistry = new StoreRegistry()
64
+ const options = testStoreOptions()
65
+
66
+ const Wrapper = ({ opts }: { opts: RegistryStoreOptions<typeof schema> }) => (
67
+ <StoreRegistryProvider storeRegistry={storeRegistry}>
68
+ <React.Suspense fallback={<div data-testid="fallback" />}>
69
+ <StoreConsumer options={opts} />
70
+ </React.Suspense>
71
+ </StoreRegistryProvider>
72
+ )
73
+
74
+ let view: RenderResult | undefined
75
+ await act(async () => {
76
+ view = render(<Wrapper opts={options} />)
77
+ })
78
+ const renderedView = view ?? shouldNeverHappen('render failed')
79
+
80
+ // Wait for initial load
81
+ await waitForSuspenseResolved(renderedView)
82
+ expect(renderedView.getByTestId('ready')).toBeDefined()
83
+
84
+ // Rerender with new options object (but same storeId)
85
+ await act(async () => {
86
+ renderedView.rerender(<Wrapper opts={{ ...options }} />)
87
+ })
88
+
89
+ // Should not show fallback
90
+ expect(renderedView.queryByTestId('fallback')).toBeNull()
91
+ expect(renderedView.getByTestId('ready')).toBeDefined()
92
+
93
+ await cleanupAfterUnmount(() => renderedView.unmount())
94
+ })
95
+
96
+ it('throws when store loading fails', async () => {
97
+ const storeRegistry = new StoreRegistry()
98
+ const badOptions = testStoreOptions({
99
+ // @ts-expect-error - intentionally passing invalid adapter to trigger error
100
+ adapter: null,
101
+ })
102
+
103
+ // Pre-load the store to cache the error (error happens synchronously)
104
+ expect(() => storeRegistry.getOrLoadPromise(badOptions)).toThrow()
105
+
106
+ // Now when useStore tries to get it, it should throw synchronously
107
+ expect(() =>
108
+ renderHook(() => useStore(badOptions), {
109
+ wrapper: makeProvider(storeRegistry),
110
+ }),
111
+ ).toThrow()
112
+ })
113
+
114
+ it.each([
115
+ { label: 'non-strict mode', strictMode: false },
116
+ { label: 'strict mode', strictMode: true },
117
+ ])('works in $label', async ({ strictMode }) => {
118
+ const storeRegistry = new StoreRegistry()
119
+ const options = testStoreOptions()
120
+
121
+ let hook: RenderHookResult<Store<typeof schema>, RegistryStoreOptions<typeof schema>> | undefined
122
+ await act(async () => {
123
+ hook = renderHook(() => useStore(options), {
124
+ wrapper: makeProvider(storeRegistry, { suspense: true }),
125
+ reactStrictMode: strictMode,
126
+ })
127
+ })
128
+ const { result, unmount } = hook ?? shouldNeverHappen('renderHook failed')
129
+
130
+ // Wait for store to be ready
131
+ await waitForStoreReady(result)
132
+ expect(result.current[StoreInternalsSymbol].clientSession).toBeDefined()
133
+
134
+ await cleanupAfterUnmount(unmount)
135
+ })
136
+
137
+ it('handles switching between different storeId values', async () => {
138
+ const storeRegistry = new StoreRegistry()
139
+
140
+ const optionsA = testStoreOptions({ storeId: 'store-a' })
141
+ const optionsB = testStoreOptions({ storeId: 'store-b' })
142
+
143
+ let hook: RenderHookResult<Store<typeof schema>, RegistryStoreOptions<typeof schema>> | undefined
144
+ await act(async () => {
145
+ hook = renderHook((opts) => useStore(opts), {
146
+ initialProps: optionsA,
147
+ wrapper: makeProvider(storeRegistry, { suspense: true }),
148
+ })
149
+ })
150
+ const { result, rerender, unmount } = hook ?? shouldNeverHappen('renderHook failed')
151
+
152
+ // Wait for first store to load
153
+ await waitForStoreReady(result)
154
+ const storeA = result.current
155
+ expect(storeA[StoreInternalsSymbol].clientSession).toBeDefined()
156
+
157
+ // Switch to different storeId
158
+ await act(async () => {
159
+ rerender(optionsB)
160
+ })
161
+
162
+ // Wait for second store to load and verify it's different from the first
163
+ await waitFor(() => {
164
+ expect(result.current).not.toBe(storeA)
165
+ expect(result.current?.[StoreInternalsSymbol].clientSession).toBeDefined()
166
+ })
167
+
168
+ const storeB = result.current
169
+ expect(storeB[StoreInternalsSymbol].clientSession).toBeDefined()
170
+ expect(storeB).not.toBe(storeA)
171
+
172
+ await cleanupAfterUnmount(unmount)
173
+ })
174
+
175
+ // useStore doesn't handle unusedCacheTime=0 correctly because retain is called in useEffect (after render)
176
+ // See https://github.com/livestorejs/livestore/issues/916
177
+ it.skip('should load store with unusedCacheTime set to 0', async () => {
178
+ const storeRegistry = new StoreRegistry({ defaultOptions: { unusedCacheTime: 0 } })
179
+ const options = testStoreOptions({ unusedCacheTime: 0 })
180
+
181
+ const StoreConsumerWithVerification = ({ opts }: { opts: RegistryStoreOptions<typeof schema> }) => {
182
+ const store = useStore(opts)
183
+ // Verify store is usable - access internals to confirm it's not disposed
184
+ const clientSession = store[StoreInternalsSymbol].clientSession
185
+ return <div data-testid="ready" data-has-session={String(clientSession !== undefined)} />
186
+ }
187
+
188
+ let view: RenderResult | undefined
189
+ await act(async () => {
190
+ view = render(
191
+ <StoreRegistryProvider storeRegistry={storeRegistry}>
192
+ <React.Suspense fallback={<div data-testid="fallback" />}>
193
+ <StoreConsumerWithVerification opts={options} />
194
+ </React.Suspense>
195
+ </StoreRegistryProvider>,
196
+ )
197
+ })
198
+ const renderedView = view ?? shouldNeverHappen('render failed')
199
+
200
+ await waitForSuspenseResolved(renderedView)
201
+
202
+ // Store should be usable while component is mounted
203
+ const readyElement = renderedView.getByTestId('ready')
204
+ expect(readyElement.getAttribute('data-has-session')).toBe('true')
205
+
206
+ // Allow some time to pass to ensure store isn't prematurely disposed
207
+ await new Promise((resolve) => setTimeout(resolve, 50))
208
+
209
+ // Store should still be usable after waiting
210
+ expect(readyElement.getAttribute('data-has-session')).toBe('true')
211
+
212
+ await cleanupAfterUnmount(() => renderedView.unmount())
213
+ })
214
+ })
215
+
216
+ const StoreConsumer = ({ options }: { options: RegistryStoreOptions<any> }) => {
217
+ useStore(options)
218
+ return <div data-testid="ready" />
219
+ }
220
+
221
+ const makeProvider =
222
+ (storeRegistry: StoreRegistry, { suspense = false }: { suspense?: boolean } = {}) =>
223
+ ({ children }: { children: React.ReactNode }) => {
224
+ let content = <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
225
+
226
+ if (suspense) {
227
+ content = <React.Suspense fallback={null}>{content}</React.Suspense>
228
+ }
229
+
230
+ return content
231
+ }
232
+
233
+ let testStoreCounter = 0
234
+
235
+ const testStoreOptions = (overrides: Partial<RegistryStoreOptions<typeof schema>> = {}) =>
236
+ storeOptions({
237
+ storeId: overrides.storeId ?? `test-store-${testStoreCounter++}`,
238
+ schema,
239
+ adapter: makeInMemoryAdapter(),
240
+ ...overrides,
241
+ })
242
+
243
+ /**
244
+ * Cleans up after component unmount and waits for pending operations to settle.
245
+ *
246
+ * When components using stores unmount, the StoreRegistry schedules garbage collection
247
+ * timers for inactive stores. This helper waits for those timers to complete naturally.
248
+ */
249
+ const cleanupAfterUnmount = async (cleanup: () => void): Promise<void> => {
250
+ cleanup()
251
+ // Allow any pending microtasks/timers to settle
252
+ await new Promise((resolve) => setTimeout(resolve, 100))
253
+ }
254
+
255
+ /**
256
+ * Waits for React Suspense fallback to resolve and the actual content to render.
257
+ */
258
+ const waitForSuspenseResolved = async (view: RenderResult): Promise<void> => {
259
+ await waitFor(() => expect(view.queryByTestId('fallback')).toBeNull())
260
+ }
261
+
262
+ /**
263
+ * Waits for a store to be fully loaded and ready to use.
264
+ * The store is considered ready when it has a defined clientSession.
265
+ */
266
+ const waitForStoreReady = async (result: { current: Store<any> }): Promise<void> => {
267
+ await waitFor(() => {
268
+ expect(result.current).not.toBeNull()
269
+ expect(result.current[StoreInternalsSymbol].clientSession).toBeDefined()
270
+ })
271
+ }
package/src/useStore.ts CHANGED
@@ -1,37 +1,116 @@
1
1
  import type { LiveStoreSchema } from '@livestore/common/schema'
2
- import type { Store } from '@livestore/livestore'
2
+ import type { RegistryStoreOptions, Store } from '@livestore/livestore'
3
+ import type { Schema } from '@livestore/utils/effect'
3
4
  import React from 'react'
4
-
5
- import type { ReactApi } from './LiveStoreContext.ts'
6
- import { LiveStoreContext } from './LiveStoreContext.ts'
5
+ import { useStoreRegistry } from './StoreRegistryContext.tsx'
7
6
  import { useClientDocument } from './useClientDocument.ts'
8
7
  import { useQuery } from './useQuery.ts'
9
8
 
10
- export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSchema>): Store<TSchema> & ReactApi => {
11
- // @ts-expect-error TODO properly implement this
9
+ /**
10
+ * Returns a store instance augmented with hooks (`store.useQuery()` and `store.useClientDocument()`) for reactive queries.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * function Issue() {
15
+ * // Suspends until loaded or returns immediately if already loaded
16
+ * const issueStore = useStore(issueStoreOptions('abc123'))
17
+ * const [issue] = issueStore.useQuery(queryDb(tables.issue.select()))
18
+ *
19
+ * const toggleStatus = () =>
20
+ * issueStore.commit(
21
+ * issueEvents.issueStatusChanged({
22
+ * id: issue.id,
23
+ * status: issue.status === 'done' ? 'todo' : 'done',
24
+ * }),
25
+ * )
26
+ *
27
+ * const preloadParentIssue = (issueId: string) =>
28
+ * storeRegistry.preload({
29
+ * ...issueStoreOptions(issueId),
30
+ * unusedCacheTime: 10_000,
31
+ * })
32
+ *
33
+ * return (
34
+ * <>
35
+ * <h2>{issue.title}</h2>
36
+ * <button onClick={() => toggleStatus()}>Toggle Status</button>
37
+ * <button onMouseEnter={() => preloadParentIssue(issue.parentIssueId)}>Open Parent Issue</button>
38
+ * </>
39
+ * )
40
+ * }
41
+ * ```
42
+ *
43
+ * @remarks
44
+ * - Suspends until the store is loaded.
45
+ * - Store is cached by its `storeId` in the `StoreRegistry`. Multiple calls with the same `storeId` return the same store instance.
46
+ * - Store is cached as long as it's being used, and after `unusedCacheTime` expires (default `60_000` ms in browser, `Infinity` in non-browser)
47
+ * - Default store options can be configured in `StoreRegistry` constructor.
48
+ * - 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.
49
+ *
50
+ * @typeParam TSchema - The schema type for the store
51
+ * @returns The loaded store instance augmented with React hooks
52
+ * @throws unknown - store loading error or if called outside `<StoreRegistryProvider>`
53
+ */
54
+ export const useStore = <
55
+ TSchema extends LiveStoreSchema,
56
+ TContext = {},
57
+ TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue,
58
+ >(
59
+ options: RegistryStoreOptions<TSchema, TContext, TSyncPayloadSchema>,
60
+ ): Store<TSchema, TContext> & ReactApi => {
61
+ const storeRegistry = useStoreRegistry()
12
62
 
13
- store.useQuery = (queryable) => useQuery(queryable, { store })
14
- // @ts-expect-error TODO properly implement this
63
+ // NOTE: retain() is called in useEffect (after render), while getOrLoadPromise() is called
64
+ // in useMemo (during render). This creates a timing gap where with very short unusedCacheTime
65
+ // values (e.g., 0), the store could theoretically be disposed before the effect fires.
66
+ // In practice, this is not an issue with the default 60s cache time, but it becomes an issue when
67
+ // `unusedCacheTime` is configured to values less than ~100ms.
68
+ // See https://github.com/livestorejs/livestore/issues/916
69
+ React.useEffect(() => storeRegistry.retain(options), [storeRegistry, options])
15
70
 
16
- store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
17
- return store as Store<TSchema> & ReactApi
18
- }
71
+ const storeOrPromise = React.useMemo(() => storeRegistry.getOrLoadPromise(options), [storeRegistry, options])
19
72
 
20
- export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {
21
- if (options?.store !== undefined) {
22
- return { store: withReactApi(options.store) }
73
+ const store = storeOrPromise instanceof Promise ? React.use(storeOrPromise) : storeOrPromise
74
+
75
+ // Expose store on the global object for browser console debugging.
76
+ globalThis.__debugLiveStore ??= {}
77
+ if (Object.keys(globalThis.__debugLiveStore).length === 0) {
78
+ globalThis.__debugLiveStore._ = store
23
79
  }
80
+ globalThis.__debugLiveStore[options.debug?.instanceId ?? options.storeId] = store
24
81
 
25
- // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
26
- const storeContext = React.useContext(LiveStoreContext)
82
+ return withReactApi(store)
83
+ }
27
84
 
28
- if (storeContext === undefined) {
29
- throw new Error(`useStore can only be used inside StoreContext.Provider`)
30
- }
85
+ /**
86
+ * React-specific methods added to the Store when used via React hooks.
87
+ *
88
+ * These methods are attached by `withReactApi()` and `useStore()`, allowing you
89
+ * to call `store.useQuery()` and `store.useClientDocument()` directly on the
90
+ * Store instance.
91
+ */
92
+ export type ReactApi = {
93
+ /** Hook version of query subscription—re-renders component when query result changes */
94
+ useQuery: typeof useQuery
95
+ /** Hook for reading and writing client-document tables with React state semantics */
96
+ useClientDocument: typeof useClientDocument
97
+ }
31
98
 
32
- if (storeContext.stage !== 'running') {
33
- throw new Error(`useStore can only be used after the store is running`)
34
- }
99
+ /**
100
+ * Augments a Store instance with React-specific methods (`useQuery`, `useClientDocument`).
101
+ *
102
+ * This is called automatically by `useStore()`. You typically don't need to call it
103
+ * directly unless you're building custom integrations.
104
+ *
105
+ * @internal
106
+ */
107
+ export const withReactApi = <TSchema extends LiveStoreSchema, TContext = {}>(
108
+ store: Store<TSchema, TContext>,
109
+ ): Store<TSchema, TContext> & ReactApi => {
110
+ // @ts-expect-error TODO properly implement this
111
+ store.useQuery = (queryable) => useQuery(queryable, { store })
35
112
 
36
- return { store: withReactApi(storeContext.store) }
113
+ // @ts-expect-error TODO properly implement this
114
+ store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
115
+ return store as Store<TSchema, TContext> & ReactApi
37
116
  }
@@ -1,13 +0,0 @@
1
- import type { LiveStoreContextRunning } from '@livestore/livestore';
2
- import React from 'react';
3
- import type { useClientDocument } from './useClientDocument.ts';
4
- import type { useQuery } from './useQuery.ts';
5
- export type ReactApi = {
6
- useQuery: typeof useQuery;
7
- useClientDocument: typeof useClientDocument;
8
- };
9
- export declare const LiveStoreContext: React.Context<{
10
- stage: "running";
11
- store: LiveStoreContextRunning["store"] & ReactApi;
12
- } | undefined>;
13
- //# sourceMappingURL=LiveStoreContext.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LiveStoreContext.d.ts","sourceRoot":"","sources":["../src/LiveStoreContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AACnE,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7C,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,OAAO,QAAQ,CAAA;IACzB,iBAAiB,EAAE,OAAO,iBAAiB,CAAA;CAC5C,CAAA;AAED,eAAO,MAAM,gBAAgB;WAClB,SAAS;WAAS,uBAAuB,CAAC,OAAO,CAAC,GAAG,QAAQ;cAC5D,CAAA"}
@@ -1,3 +0,0 @@
1
- import React from 'react';
2
- export const LiveStoreContext = React.createContext(undefined);
3
- //# sourceMappingURL=LiveStoreContext.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LiveStoreContext.js","sourceRoot":"","sources":["../src/LiveStoreContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC,aAAa,CAEjD,SAAS,CAAC,CAAA"}
@@ -1,66 +0,0 @@
1
- import type { Adapter, BootStatus, IntentionalShutdownCause, MigrationsReport, SyncError } from '@livestore/common';
2
- import { LogConfig, UnknownError } from '@livestore/common';
3
- import type { LiveStoreSchema } from '@livestore/common/schema';
4
- import type { OtelOptions, Store } from '@livestore/livestore';
5
- import { StoreInterrupted } from '@livestore/livestore';
6
- import type { OtelTracer } from '@livestore/utils/effect';
7
- import { Effect, Schema } from '@livestore/utils/effect';
8
- import type * as otel from '@opentelemetry/api';
9
- import React from 'react';
10
- export interface LiveStoreProviderProps<TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue> extends LogConfig.WithLoggerOptions {
11
- schema: LiveStoreSchema;
12
- /**
13
- * The `storeId` can be used to isolate multiple stores from each other.
14
- * So it can be useful for multi-tenancy scenarios.
15
- *
16
- * The `storeId` is also used for persistence.
17
- *
18
- * Make sure to also configure `storeId` in LiveStore Devtools (e.g. in Vite plugin).
19
- *
20
- * @default 'default'
21
- */
22
- storeId?: string;
23
- boot?: (store: Store<LiveStoreSchema>, ctx: {
24
- migrationsReport: MigrationsReport;
25
- parentSpan: otel.Span;
26
- }) => void | Promise<void> | Effect.Effect<void, unknown, OtelTracer.OtelTracer>;
27
- otelOptions?: Partial<OtelOptions>;
28
- renderLoading?: (status: BootStatus) => React.ReactNode;
29
- renderError?: (error: UnknownError | unknown) => React.ReactNode;
30
- renderShutdown?: (cause: IntentionalShutdownCause | StoreInterrupted | SyncError) => React.ReactNode;
31
- adapter: Adapter;
32
- /**
33
- * In order for LiveStore to apply multiple events in a single render,
34
- * you need to pass the `batchUpdates` function from either `react-dom` or `react-native`.
35
- *
36
- * ```ts
37
- * // With React DOM
38
- * import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
39
- *
40
- * // With React Native
41
- * import { unstable_batchedUpdates as batchUpdates } from 'react-native'
42
- * ```
43
- */
44
- batchUpdates: (run: () => void) => void;
45
- disableDevtools?: boolean;
46
- signal?: AbortSignal;
47
- /**
48
- * Currently only used in the web adapter:
49
- * If true, registers a beforeunload event listener to confirm unsaved changes.
50
- *
51
- * @default true
52
- */
53
- confirmUnsavedChanges?: boolean;
54
- /**
55
- * Payload that will be passed to the sync backend when connecting
56
- *
57
- * @default undefined
58
- */
59
- syncPayloadSchema?: TSyncPayloadSchema;
60
- syncPayload?: Schema.Schema.Type<TSyncPayloadSchema>;
61
- debug?: {
62
- instanceId?: string;
63
- };
64
- }
65
- export declare const LiveStoreProvider: <TSyncPayloadSchema extends Schema.Schema<any> = typeof Schema.JsonValue>({ renderLoading, renderError, renderShutdown, otelOptions, children, schema, storeId, boot, adapter, batchUpdates, disableDevtools, signal, confirmUnsavedChanges, syncPayload, syncPayloadSchema, debug, logger, logLevel, }: LiveStoreProviderProps<TSyncPayloadSchema> & React.PropsWithChildren) => React.ReactNode;
66
- //# sourceMappingURL=LiveStoreProvider.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LiveStoreProvider.d.ts","sourceRoot":"","sources":["../src/LiveStoreProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AACnH,OAAO,EAAE,SAAS,EAAe,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAEV,WAAW,EAEX,KAAK,EAEN,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAqC,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAmB,MAAM,EAAkB,MAAM,EAAsB,MAAM,yBAAyB,CAAA;AAC7G,OAAO,KAAK,KAAK,IAAI,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,MAAM,WAAW,sBAAsB,CAAC,kBAAkB,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,OAAO,MAAM,CAAC,SAAS,CAC7G,SAAQ,SAAS,CAAC,iBAAiB;IACnC,MAAM,EAAE,eAAe,CAAA;IACvB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,CACL,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,EAC7B,GAAG,EAAE;QAAE,gBAAgB,EAAE,gBAAgB,CAAC;QAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAA;KAAE,KAC/D,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC,CAAA;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAClC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;IACvD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAChE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,wBAAwB,GAAG,gBAAgB,GAAG,SAAS,KAAK,KAAK,CAAC,SAAS,CAAA;IACpG,OAAO,EAAE,OAAO,CAAA;IAChB;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;IACvC,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,kBAAkB,CAAA;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;IACpD,KAAK,CAAC,EAAE;QACN,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AA2BD,eAAO,MAAM,iBAAiB,GAAI,kBAAkB,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,OAAO,MAAM,CAAC,SAAS,EAAE,+NAmBxG,sBAAsB,CAAC,kBAAkB,CAAC,GAAG,KAAK,CAAC,iBAAiB,KAAG,KAAK,CAAC,SAuC/E,CAAA"}