@livestore/react 0.4.0-dev.2 → 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.
Files changed (96) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +27 -0
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +18 -0
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +14 -8
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +40 -24
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/LiveStoreProvider.test.js +7 -7
  11. package/dist/LiveStoreProvider.test.js.map +1 -1
  12. package/dist/__tests__/fixture.d.ts +34 -12
  13. package/dist/__tests__/fixture.d.ts.map +1 -1
  14. package/dist/__tests__/fixture.js +13 -5
  15. package/dist/__tests__/fixture.js.map +1 -1
  16. package/dist/experimental/components/LiveList.js +1 -1
  17. package/dist/experimental/mod.d.ts +1 -0
  18. package/dist/experimental/mod.d.ts.map +1 -1
  19. package/dist/experimental/mod.js +1 -0
  20. package/dist/experimental/mod.js.map +1 -1
  21. package/dist/experimental/multi-store/StoreRegistry.d.ts +105 -0
  22. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -0
  23. package/dist/experimental/multi-store/StoreRegistry.js +184 -0
  24. package/dist/experimental/multi-store/StoreRegistry.js.map +1 -0
  25. package/dist/experimental/multi-store/StoreRegistry.test.d.ts +2 -0
  26. package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +1 -0
  27. package/dist/experimental/multi-store/StoreRegistry.test.js +381 -0
  28. package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -0
  29. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +10 -0
  30. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +1 -0
  31. package/dist/experimental/multi-store/StoreRegistryContext.js +15 -0
  32. package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -0
  33. package/dist/experimental/multi-store/mod.d.ts +6 -0
  34. package/dist/experimental/multi-store/mod.d.ts.map +1 -0
  35. package/dist/experimental/multi-store/mod.js +6 -0
  36. package/dist/experimental/multi-store/mod.js.map +1 -0
  37. package/dist/experimental/multi-store/storeOptions.d.ts +4 -0
  38. package/dist/experimental/multi-store/storeOptions.d.ts.map +1 -0
  39. package/dist/experimental/multi-store/storeOptions.js +4 -0
  40. package/dist/experimental/multi-store/storeOptions.js.map +1 -0
  41. package/dist/experimental/multi-store/types.d.ts +25 -0
  42. package/dist/experimental/multi-store/types.d.ts.map +1 -0
  43. package/dist/experimental/multi-store/types.js +2 -0
  44. package/dist/experimental/multi-store/types.js.map +1 -0
  45. package/dist/experimental/multi-store/useStore.d.ts +11 -0
  46. package/dist/experimental/multi-store/useStore.d.ts.map +1 -0
  47. package/dist/experimental/multi-store/useStore.js +16 -0
  48. package/dist/experimental/multi-store/useStore.js.map +1 -0
  49. package/dist/experimental/multi-store/useStore.test.d.ts +2 -0
  50. package/dist/experimental/multi-store/useStore.test.d.ts.map +1 -0
  51. package/dist/experimental/multi-store/useStore.test.js +198 -0
  52. package/dist/experimental/multi-store/useStore.test.js.map +1 -0
  53. package/dist/mod.d.ts +1 -1
  54. package/dist/mod.d.ts.map +1 -1
  55. package/dist/mod.js.map +1 -1
  56. package/dist/useClientDocument.d.ts +43 -13
  57. package/dist/useClientDocument.d.ts.map +1 -1
  58. package/dist/useClientDocument.js +4 -5
  59. package/dist/useClientDocument.js.map +1 -1
  60. package/dist/useClientDocument.test.js +29 -7
  61. package/dist/useClientDocument.test.js.map +1 -1
  62. package/dist/useQuery.d.ts +28 -6
  63. package/dist/useQuery.d.ts.map +1 -1
  64. package/dist/useQuery.js +63 -18
  65. package/dist/useQuery.js.map +1 -1
  66. package/dist/useQuery.test.js +35 -11
  67. package/dist/useQuery.test.js.map +1 -1
  68. package/dist/useRcResource.test.js +1 -1
  69. package/dist/useStore.d.ts +53 -1
  70. package/dist/useStore.d.ts.map +1 -1
  71. package/dist/useStore.js +52 -1
  72. package/dist/useStore.js.map +1 -1
  73. package/package.json +14 -14
  74. package/src/LiveStoreContext.ts +27 -0
  75. package/src/LiveStoreProvider.test.tsx +7 -7
  76. package/src/LiveStoreProvider.tsx +67 -45
  77. package/src/__snapshots__/useClientDocument.test.tsx.snap +208 -100
  78. package/src/__snapshots__/useQuery.test.tsx.snap +400 -128
  79. package/src/__tests__/fixture.tsx +23 -24
  80. package/src/experimental/components/LiveList.tsx +1 -1
  81. package/src/experimental/mod.ts +1 -0
  82. package/src/experimental/multi-store/StoreRegistry.test.ts +518 -0
  83. package/src/experimental/multi-store/StoreRegistry.ts +253 -0
  84. package/src/experimental/multi-store/StoreRegistryContext.tsx +23 -0
  85. package/src/experimental/multi-store/mod.ts +5 -0
  86. package/src/experimental/multi-store/storeOptions.ts +8 -0
  87. package/src/experimental/multi-store/types.ts +37 -0
  88. package/src/experimental/multi-store/useStore.test.tsx +269 -0
  89. package/src/experimental/multi-store/useStore.ts +26 -0
  90. package/src/mod.ts +2 -1
  91. package/src/useClientDocument.test.tsx +105 -75
  92. package/src/useClientDocument.ts +58 -13
  93. package/src/useQuery.test.tsx +62 -11
  94. package/src/useQuery.ts +98 -27
  95. package/src/useRcResource.test.tsx +1 -1
  96. package/src/useStore.ts +55 -3
package/src/useQuery.ts CHANGED
@@ -1,5 +1,14 @@
1
+ import { isQueryBuilder } from '@livestore/common'
1
2
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
2
- import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
3
+ import {
4
+ extractStackInfoFromStackTrace,
5
+ isQueryable,
6
+ type Queryable,
7
+ queryDb,
8
+ type SignalDef,
9
+ StoreInternalsSymbol,
10
+ stackInfoToString,
11
+ } from '@livestore/livestore'
3
12
  import type { LiveQueries } from '@livestore/livestore/internal'
4
13
  import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
5
14
  import * as otel from '@opentelemetry/api'
@@ -21,15 +30,36 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
21
30
  * }
22
31
  * ```
23
32
  */
24
- export const useQuery = <TQuery extends LiveQueryDef.Any>(
25
- queryDef: TQuery,
33
+ export const useQuery = <TQueryable extends Queryable<any>>(
34
+ queryable: TQueryable,
26
35
  options?: { store?: Store },
27
- ): LiveQueries.GetResult<TQuery> => useQueryRef(queryDef, options).valueRef.current
36
+ ): Queryable.Result<TQueryable> => useQueryRef(queryable, options).valueRef.current
28
37
 
29
38
  /**
39
+ * Like `useQuery`, but also returns a reference to the underlying LiveQuery instance.
40
+ *
41
+ * Usage
42
+ * - Accepts any `Queryable<TResult>`: a `LiveQueryDef`, `SignalDef`, a `LiveQuery` instance
43
+ * or a SQL `QueryBuilder`. Unions of queryables are supported and the result type is
44
+ * inferred via `Queryable.Result<TQueryable>`.
45
+ * - Creates an OpenTelemetry span per unique query, reusing it while the ref-counted
46
+ * resource is alive. The span name is updated once the dynamic label is known.
47
+ * - Manages a reference-counted resource under-the-hood so query instances are shared
48
+ * across re-renders and properly disposed once no longer referenced.
49
+ *
50
+ * Parameters
51
+ * - `queryable`: The query definition/instance/builder to run and subscribe to.
52
+ * - `options.store`: Optional store to use; by default the store from `LiveStoreContext` is used.
53
+ * - `options.otelContext`: Optional parent otel context for the query span.
54
+ * - `options.otelSpanName`: Optional explicit span name; otherwise derived from the query label.
55
+ *
56
+ * Returns
57
+ * - `valueRef`: A React ref whose `current` holds the latest query result. The type is
58
+ * `Queryable.Result<TQueryable>` with full inference for unions.
59
+ * - `queryRcRef`: The underlying reference-counted `LiveQuery` instance used by the store.
30
60
  */
31
- export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
32
- queryDef: TQuery,
61
+ export const useQueryRef = <TQueryable extends Queryable<any>>(
62
+ queryable: TQueryable,
33
63
  options?: {
34
64
  store?: Store
35
65
  /** Parent otel context for the query */
@@ -38,17 +68,50 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
38
68
  otelSpanName?: string
39
69
  },
40
70
  ): {
41
- valueRef: React.RefObject<LiveQueries.GetResult<TQuery>>
42
- queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>>>
71
+ valueRef: React.RefObject<Queryable.Result<TQueryable>>
72
+ queryRcRef: LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>
43
73
  } => {
44
74
  const store =
45
- options?.store ??
46
- // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
75
+ options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
47
76
  React.useContext(LiveStoreContext)?.store ??
48
77
  shouldNeverHappen(`No store provided to useQuery`)
49
78
 
79
+ type TResult = Queryable.Result<TQueryable>
80
+ type NormalizedQueryable =
81
+ | { _tag: 'definition'; def: LiveQueryDef<TResult> | SignalDef<TResult> }
82
+ | { _tag: 'live-query'; query$: LiveQuery<TResult> }
83
+
84
+ const normalized = React.useMemo<NormalizedQueryable>(() => {
85
+ if (!isQueryable(queryable)) {
86
+ return shouldNeverHappen('useQuery expected a Queryable value')
87
+ }
88
+
89
+ if (isQueryBuilder(queryable)) {
90
+ return { _tag: 'definition', def: queryDb(queryable) }
91
+ }
92
+
93
+ if (
94
+ (queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'def' ||
95
+ (queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'signal-def'
96
+ ) {
97
+ return { _tag: 'definition', def: queryable as LiveQueryDef<TResult> | SignalDef<TResult> }
98
+ }
99
+
100
+ return { _tag: 'live-query', query$: queryable as LiveQuery<TResult> }
101
+ }, [queryable])
102
+
50
103
  // It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
51
- const rcRefKey = `${store.storeId}_${store.clientId}_${store.sessionId}_${queryDef.hash}`
104
+ const rcRefKey = React.useMemo(() => {
105
+ const base = `${store.storeId}_${store.clientId}_${store.sessionId}`
106
+
107
+ if (normalized._tag === 'definition') {
108
+ return `${base}:def:${normalized.def.hash}`
109
+ }
110
+
111
+ return `${base}:instance:${normalized.query$.id}`
112
+ }, [normalized, store.clientId, store.sessionId, store.storeId])
113
+
114
+ const resourceLabel = normalized._tag === 'definition' ? normalized.def.label : normalized.query$.label
52
115
 
53
116
  const stackInfo = React.useMemo(() => {
54
117
  Error.stackTraceLimit = 10
@@ -60,17 +123,22 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
60
123
  const { queryRcRef, span, otelContext } = useRcResource(
61
124
  rcRefKey,
62
125
  () => {
63
- const queryDefLabel = queryDef.label
64
-
65
- const span = store.otel.tracer.startSpan(
66
- options?.otelSpanName ?? `LiveStore:useQuery:${queryDefLabel}`,
67
- { attributes: { label: queryDefLabel, firstStackInfo: JSON.stringify(stackInfo) } },
68
- options?.otelContext ?? store.otel.queriesSpanContext,
126
+ const span = store[StoreInternalsSymbol].otel.tracer.startSpan(
127
+ options?.otelSpanName ?? `LiveStore:useQuery:${resourceLabel}`,
128
+ { attributes: { label: resourceLabel, firstStackInfo: JSON.stringify(stackInfo) } },
129
+ options?.otelContext ?? store[StoreInternalsSymbol].otel.queriesSpanContext,
69
130
  )
70
131
 
71
132
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
72
133
 
73
- const queryRcRef = queryDef.make(store.reactivityGraph.context!, otelContext)
134
+ const queryRcRef =
135
+ normalized._tag === 'definition'
136
+ ? normalized.def.make(store[StoreInternalsSymbol].reactivityGraph.context!, otelContext)
137
+ : ({
138
+ value: normalized.query$,
139
+ deref: () => {},
140
+ rc: Number.POSITIVE_INFINITY,
141
+ } satisfies LiveQueries.RcRef<LiveQuery<TResult>>)
74
142
 
75
143
  return { queryRcRef, span, otelContext }
76
144
  },
@@ -83,7 +151,7 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
83
151
  // const queryRcRef.value.get()
84
152
  // }
85
153
 
86
- const query$ = queryRcRef.value as LiveQuery<LiveQueries.GetResult<TQuery>>
154
+ const query$ = queryRcRef.value as LiveQuery<TResult>
87
155
 
88
156
  React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
89
157
  // console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
@@ -119,7 +187,7 @@ Stack trace:
119
187
  }, [otelContext, query$, stackInfo])
120
188
 
121
189
  // We know the query has a result by the time we use it; so we can synchronously populate a default state
122
- const [valueRef, setValue] = useStateRefWithReactiveInput<LiveQueries.GetResult<TQuery>>(initialResult)
190
+ const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
123
191
 
124
192
  // TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
125
193
  // before calling the LS `onEffect` on it
@@ -133,8 +201,9 @@ Stack trace:
133
201
  // so we're also updating the span name here.
134
202
  span.updateName(options?.otelSpanName ?? `LiveStore:useQuery:${query$.label}`)
135
203
 
136
- return store.subscribe(query$, {
137
- onUpdate: (newValue) => {
204
+ return store.subscribe(
205
+ query$,
206
+ (newValue) => {
138
207
  // NOTE: we return a reference to the result object within LiveStore;
139
208
  // this implies that app code must not mutate the results, or else
140
209
  // there may be weird reactivity bugs.
@@ -142,12 +211,14 @@ Stack trace:
142
211
  setValue(newValue)
143
212
  }
144
213
  },
145
- onUnsubsubscribe: () => {
146
- query$.activeSubscriptions.delete(stackInfo)
214
+ {
215
+ onUnsubsubscribe: () => {
216
+ query$.activeSubscriptions.delete(stackInfo)
217
+ },
218
+ label: query$.label,
219
+ otelContext,
147
220
  },
148
- label: query$.label,
149
- otelContext,
150
- })
221
+ )
151
222
  }, [stackInfo, query$, setValue, store, valueRef, otelContext, span, options?.otelSpanName])
152
223
 
153
224
  useRcResource(
@@ -2,7 +2,7 @@ import * as ReactTesting from '@testing-library/react'
2
2
  import * as React from 'react'
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
4
4
 
5
- import { __resetUseRcResourceCache, useRcResource } from './useRcResource.js'
5
+ import { __resetUseRcResourceCache, useRcResource } from './useRcResource.ts'
6
6
 
7
7
  describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (strictMode=%s)', ({ strictMode }) => {
8
8
  beforeEach(() => {
package/src/useStore.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { LiveStoreSchema } from '@livestore/common/schema'
1
2
  import type { Store } from '@livestore/livestore'
2
3
  import React from 'react'
3
4
 
@@ -6,16 +7,67 @@ import { LiveStoreContext } from './LiveStoreContext.ts'
6
7
  import { useClientDocument } from './useClientDocument.ts'
7
8
  import { useQuery } from './useQuery.ts'
8
9
 
9
- export const withReactApi = (store: Store): Store & ReactApi => {
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
+ */
23
+ export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSchema>): Store<TSchema> & ReactApi => {
10
24
  // @ts-expect-error TODO properly implement this
11
25
 
12
- store.useQuery = (queryDef) => useQuery(queryDef, { store })
26
+ store.useQuery = (queryable) => useQuery(queryable, { store })
13
27
  // @ts-expect-error TODO properly implement this
14
28
 
15
29
  store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
16
- return store as Store & ReactApi
30
+ return store as Store<TSchema> & ReactApi
17
31
  }
18
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
+ */
19
71
  export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {
20
72
  if (options?.store !== undefined) {
21
73
  return { store: withReactApi(options.store) }