@livestore/react 0.4.0-dev.21 → 0.4.0-dev.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/StoreRegistryContext.d.ts +56 -0
  4. package/dist/StoreRegistryContext.d.ts.map +1 -0
  5. package/dist/StoreRegistryContext.js +61 -0
  6. package/dist/StoreRegistryContext.js.map +1 -0
  7. package/dist/__tests__/fixture.d.ts +8 -280
  8. package/dist/__tests__/fixture.d.ts.map +1 -1
  9. package/dist/__tests__/fixture.js +9 -84
  10. package/dist/__tests__/fixture.js.map +1 -1
  11. package/dist/experimental/components/LiveList.d.ts +4 -2
  12. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  13. package/dist/experimental/components/LiveList.js +9 -7
  14. package/dist/experimental/components/LiveList.js.map +1 -1
  15. package/dist/experimental/mod.d.ts +0 -1
  16. package/dist/experimental/mod.d.ts.map +1 -1
  17. package/dist/experimental/mod.js +0 -1
  18. package/dist/experimental/mod.js.map +1 -1
  19. package/dist/mod.d.ts +8 -5
  20. package/dist/mod.d.ts.map +1 -1
  21. package/dist/mod.js +6 -4
  22. package/dist/mod.js.map +1 -1
  23. package/dist/useClientDocument.d.ts +1 -26
  24. package/dist/useClientDocument.d.ts.map +1 -1
  25. package/dist/useClientDocument.js +3 -17
  26. package/dist/useClientDocument.js.map +1 -1
  27. package/dist/useClientDocument.test.js +12 -4
  28. package/dist/useClientDocument.test.js.map +1 -1
  29. package/dist/useQuery.d.ts +4 -5
  30. package/dist/useQuery.d.ts.map +1 -1
  31. package/dist/useQuery.js +12 -85
  32. package/dist/useQuery.js.map +1 -1
  33. package/dist/useQuery.test.js +7 -8
  34. package/dist/useQuery.test.js.map +1 -1
  35. package/dist/useRcResource.d.ts.map +1 -1
  36. package/dist/useRcResource.js +9 -5
  37. package/dist/useRcResource.js.map +1 -1
  38. package/dist/useRcResource.test.js +1 -1
  39. package/dist/useRcResource.test.js.map +1 -1
  40. package/dist/useStore.d.ts +61 -46
  41. package/dist/useStore.d.ts.map +1 -1
  42. package/dist/useStore.js +75 -60
  43. package/dist/useStore.js.map +1 -1
  44. package/dist/useStore.test.d.ts.map +1 -0
  45. package/dist/{experimental/multi-store/useStore.test.js → useStore.test.js} +70 -27
  46. package/dist/useStore.test.js.map +1 -0
  47. package/dist/useSyncStatus.d.ts +22 -0
  48. package/dist/useSyncStatus.d.ts.map +1 -0
  49. package/dist/useSyncStatus.js +28 -0
  50. package/dist/useSyncStatus.js.map +1 -0
  51. package/package.json +69 -26
  52. package/src/StoreRegistryContext.tsx +70 -0
  53. package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
  54. package/src/__tests__/fixture.tsx +23 -118
  55. package/src/experimental/components/LiveList.tsx +22 -9
  56. package/src/experimental/mod.ts +0 -1
  57. package/src/mod.ts +8 -12
  58. package/src/useClientDocument.test.tsx +16 -6
  59. package/src/useClientDocument.ts +7 -61
  60. package/src/useQuery.test.tsx +8 -8
  61. package/src/useQuery.ts +30 -119
  62. package/src/useRcResource.test.tsx +1 -1
  63. package/src/useRcResource.ts +10 -5
  64. package/src/{experimental/multi-store/useStore.test.tsx → useStore.test.tsx} +117 -39
  65. package/src/useStore.ts +106 -65
  66. package/src/useSyncStatus.ts +34 -0
  67. package/dist/LiveStoreContext.d.ts +0 -40
  68. package/dist/LiveStoreContext.d.ts.map +0 -1
  69. package/dist/LiveStoreContext.js +0 -21
  70. package/dist/LiveStoreContext.js.map +0 -1
  71. package/dist/LiveStoreProvider.d.ts +0 -73
  72. package/dist/LiveStoreProvider.d.ts.map +0 -1
  73. package/dist/LiveStoreProvider.js +0 -233
  74. package/dist/LiveStoreProvider.js.map +0 -1
  75. package/dist/LiveStoreProvider.test.d.ts +0 -2
  76. package/dist/LiveStoreProvider.test.d.ts.map +0 -1
  77. package/dist/LiveStoreProvider.test.js +0 -117
  78. package/dist/LiveStoreProvider.test.js.map +0 -1
  79. package/dist/experimental/multi-store/StoreRegistry.d.ts +0 -105
  80. package/dist/experimental/multi-store/StoreRegistry.d.ts.map +0 -1
  81. package/dist/experimental/multi-store/StoreRegistry.js +0 -184
  82. package/dist/experimental/multi-store/StoreRegistry.js.map +0 -1
  83. package/dist/experimental/multi-store/StoreRegistry.test.d.ts +0 -2
  84. package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +0 -1
  85. package/dist/experimental/multi-store/StoreRegistry.test.js +0 -381
  86. package/dist/experimental/multi-store/StoreRegistry.test.js.map +0 -1
  87. package/dist/experimental/multi-store/StoreRegistryContext.d.ts +0 -10
  88. package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +0 -1
  89. package/dist/experimental/multi-store/StoreRegistryContext.js +0 -15
  90. package/dist/experimental/multi-store/StoreRegistryContext.js.map +0 -1
  91. package/dist/experimental/multi-store/mod.d.ts +0 -6
  92. package/dist/experimental/multi-store/mod.d.ts.map +0 -1
  93. package/dist/experimental/multi-store/mod.js +0 -6
  94. package/dist/experimental/multi-store/mod.js.map +0 -1
  95. package/dist/experimental/multi-store/storeOptions.d.ts +0 -4
  96. package/dist/experimental/multi-store/storeOptions.d.ts.map +0 -1
  97. package/dist/experimental/multi-store/storeOptions.js +0 -4
  98. package/dist/experimental/multi-store/storeOptions.js.map +0 -1
  99. package/dist/experimental/multi-store/types.d.ts +0 -25
  100. package/dist/experimental/multi-store/types.d.ts.map +0 -1
  101. package/dist/experimental/multi-store/types.js +0 -2
  102. package/dist/experimental/multi-store/types.js.map +0 -1
  103. package/dist/experimental/multi-store/useStore.d.ts +0 -11
  104. package/dist/experimental/multi-store/useStore.d.ts.map +0 -1
  105. package/dist/experimental/multi-store/useStore.js +0 -16
  106. package/dist/experimental/multi-store/useStore.js.map +0 -1
  107. package/dist/experimental/multi-store/useStore.test.d.ts.map +0 -1
  108. package/dist/experimental/multi-store/useStore.test.js.map +0 -1
  109. package/dist/utils/stack-info.d.ts +0 -4
  110. package/dist/utils/stack-info.d.ts.map +0 -1
  111. package/dist/utils/stack-info.js +0 -10
  112. package/dist/utils/stack-info.js.map +0 -1
  113. package/src/LiveStoreContext.ts +0 -41
  114. package/src/LiveStoreProvider.test.tsx +0 -248
  115. package/src/LiveStoreProvider.tsx +0 -430
  116. package/src/ambient.d.ts +0 -1
  117. package/src/experimental/multi-store/StoreRegistry.test.ts +0 -518
  118. package/src/experimental/multi-store/StoreRegistry.ts +0 -253
  119. package/src/experimental/multi-store/StoreRegistryContext.tsx +0 -23
  120. package/src/experimental/multi-store/mod.ts +0 -5
  121. package/src/experimental/multi-store/storeOptions.ts +0 -8
  122. package/src/experimental/multi-store/types.ts +0 -37
  123. package/src/experimental/multi-store/useStore.ts +0 -26
  124. package/src/utils/stack-info.ts +0 -13
  125. /package/dist/{experimental/multi-store/useStore.test.d.ts → useStore.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { LiveQueryDef } from '@livestore/livestore'
1
+ import type { LiveQueryDef, Store } from '@livestore/livestore'
2
2
  import { computed } from '@livestore/livestore'
3
3
  import React from 'react'
4
4
 
@@ -16,6 +16,8 @@ export type LiveListProps<TItem> = {
16
16
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
17
17
  /** Needs to be unique across all list items */
18
18
  getKey: (item: TItem, index: number) => string | number
19
+ /** The store instance to use for queries */
20
+ store: Store<any, any>
19
21
  }
20
22
 
21
23
  /**
@@ -26,12 +28,15 @@ export type LiveListProps<TItem> = {
26
28
  * In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
27
29
  * e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
28
30
  */
29
- export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
31
+ export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveListProps<TItem>): React.ReactNode => {
30
32
  const [hasMounted, setHasMounted] = React.useState(false)
31
33
 
32
34
  React.useEffect(() => setHasMounted(true), [])
33
35
 
34
- const keys = useQuery(computed((get) => get(items$).map(getKey)))
36
+ const keys = useQuery(
37
+ computed((get) => get(items$).map(getKey)),
38
+ { store },
39
+ )
35
40
  const arr = React.useMemo(
36
41
  () =>
37
42
  keys.map(
@@ -54,7 +59,9 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
54
59
  key={key}
55
60
  itemKey={key}
56
61
  item$={item$}
57
- opts={{ isInitialListRender: !hasMounted, index }}
62
+ store={store}
63
+ index={index}
64
+ isInitialListRender={!hasMounted}
58
65
  renderItem={renderItem}
59
66
  />
60
67
  ))}
@@ -64,15 +71,20 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
64
71
 
65
72
  const ItemWrapper = <TItem,>({
66
73
  item$,
67
- opts,
74
+ index,
75
+ isInitialListRender,
68
76
  renderItem,
77
+ store,
69
78
  }: {
70
79
  itemKey: string | number
71
80
  item$: LiveQueryDef<TItem>
72
- opts: { index: number; isInitialListRender: boolean }
81
+ index: number
82
+ isInitialListRender: boolean
73
83
  renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
84
+ store: Store<any, any>
74
85
  }) => {
75
- const item = useQuery(item$)
86
+ const item = useQuery(item$, { store })
87
+ const opts = React.useMemo(() => ({ index, isInitialListRender }), [index, isInitialListRender])
76
88
 
77
89
  return <>{renderItem(item, opts)}</>
78
90
  }
@@ -82,6 +94,7 @@ const ItemWrapperMemo = React.memo(
82
94
  (prev, next) =>
83
95
  prev.itemKey === next.itemKey &&
84
96
  prev.renderItem === next.renderItem &&
85
- prev.opts.index === next.opts.index &&
86
- prev.opts.isInitialListRender === next.opts.isInitialListRender,
97
+ prev.store === next.store &&
98
+ prev.index === next.index &&
99
+ prev.isInitialListRender === next.isInitialListRender,
87
100
  ) as typeof ItemWrapper
@@ -1,2 +1 @@
1
1
  export { LiveList, type LiveListProps } from './components/LiveList.tsx'
2
- export * from './multi-store/mod.ts'
package/src/mod.ts CHANGED
@@ -1,13 +1,9 @@
1
- export { LiveStoreContext, type ReactApi } from './LiveStoreContext.ts'
2
- export { LiveStoreProvider } from './LiveStoreProvider.tsx'
3
- export {
4
- type Dispatch,
5
- type SetStateAction,
6
- type SetStateActionPartial,
7
- type StateSetters,
8
- type UseClientDocumentResult,
9
- useClientDocument,
10
- } from './useClientDocument.ts'
1
+ export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
2
+ export { captureStackInfo } from '@livestore/framework-toolkit'
3
+ export { StoreRegistry, storeOptions } from '@livestore/livestore'
4
+ export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
5
+ export * from './StoreRegistryContext.tsx'
6
+ export { type UseClientDocumentResult, useClientDocument } from './useClientDocument.ts'
11
7
  export { useQuery, useQueryRef } from './useQuery.ts'
12
- export { useStore, withReactApi } from './useStore.ts'
13
- export { useStackInfo } from './utils/stack-info.ts'
8
+ export { type ReactApi, useStore, withReactApi } from './useStore.ts'
9
+ export { useSyncStatus } from './useSyncStatus.ts'
@@ -8,7 +8,7 @@ import { Vitest } from '@livestore/utils-dev/node-vitest'
8
8
  import * as otel from '@opentelemetry/api'
9
9
  import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
10
10
  import * as ReactTesting from '@testing-library/react'
11
- import type React from 'react'
11
+ import * as React from 'react'
12
12
  import { beforeEach, expect, it } from 'vitest'
13
13
 
14
14
  import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
@@ -120,25 +120,35 @@ Vitest.describe('useClientDocument', () => {
120
120
  renderCount.inc()
121
121
 
122
122
  const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
123
+ const setCurrentTaskId = React.useCallback((taskId: string) => setState({ currentTaskId: taskId }), [setState])
123
124
 
124
125
  globalSetState = setState
125
126
 
126
127
  return (
127
128
  <div>
128
- <TasksList setTaskId={(taskId) => setState({ currentTaskId: taskId })} />
129
+ <TasksList setTaskId={setCurrentTaskId} />
129
130
  <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
130
- {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
131
+ {state.currentTaskId !== null ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
131
132
  </div>
132
133
  )
133
134
  }
134
135
 
135
136
  const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
136
137
  const allTodos = store.useQuery(allTodos$)
138
+ const handleTaskClick = React.useCallback(
139
+ (event: React.MouseEvent<HTMLDivElement>) => {
140
+ const taskId = event.currentTarget.dataset.taskId
141
+ if (taskId !== undefined) {
142
+ setTaskId(taskId)
143
+ }
144
+ },
145
+ [setTaskId],
146
+ )
137
147
 
138
148
  return (
139
149
  <div>
140
150
  {allTodos.map((_) => (
141
- <div key={_.id} onClick={() => setTaskId(_.id)}>
151
+ <div key={_.id} data-task-id={_.id} onClick={handleTaskClick}>
142
152
  {_.id}
143
153
  </div>
144
154
  ))}
@@ -264,7 +274,7 @@ Vitest.describe('useClientDocument', () => {
264
274
  spanProcessors: [new SimpleSpanProcessor(exporter)],
265
275
  })
266
276
 
267
- const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
277
+ const otelTracer = provider.getTracer(`testing-${strictMode !== undefined ? 'strict' : 'non-strict'}`)
268
278
 
269
279
  const span = otelTracer.startSpan('test-root')
270
280
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -314,7 +324,7 @@ Vitest.describe('useClientDocument', () => {
314
324
  const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
315
325
  // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
316
326
  stackInfo.frames.forEach((_) => {
317
- if (_.name.includes('renderHook.wrapper')) {
327
+ if (_.name.includes('renderHook.wrapper') === true) {
318
328
  _.name = 'renderHook.wrapper'
319
329
  }
320
330
  _.filePath = '__REPLACED_FOR_SNAPSHOT__'
@@ -1,12 +1,13 @@
1
+ import React from 'react'
2
+
1
3
  import type { RowQuery } from '@livestore/common'
2
4
  import { SessionIdSymbol } from '@livestore/common'
3
5
  import { State } from '@livestore/common/schema'
6
+ import { removeUndefinedValues, type StateSetters, validateTableOptions } from '@livestore/framework-toolkit'
4
7
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
5
8
  import { queryDb } from '@livestore/livestore'
6
9
  import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
7
- import React from 'react'
8
10
 
9
- import { LiveStoreContext } from './LiveStoreContext.ts'
10
11
  import { useQueryRef } from './useQuery.ts'
11
12
 
12
13
  /**
@@ -115,20 +116,15 @@ export const useClientDocument: {
115
116
 
116
117
  const tableName = table.sqliteDef.name
117
118
 
118
- const store =
119
- storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
120
- React.useContext(LiveStoreContext)?.store ??
121
- shouldNeverHappen(`No store provided to useClientDocument`)
122
-
123
- // console.debug('useClientDocument', tableName, id)
119
+ const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
124
120
 
125
121
  const idStr: string = id === SessionIdSymbol ? store.sessionId : id
126
122
 
127
123
  type QueryDef = LiveQueryDef<TTableDef['Value']>
128
124
  const queryDef: QueryDef = React.useMemo(
129
125
  () =>
130
- queryDb(table.get(id!, { default: defaultValues! }), {
131
- deps: [idStr!, table.sqliteDef.name, JSON.stringify(defaultValues)],
126
+ queryDb(table.get(id, { default: defaultValues! }), {
127
+ deps: [idStr, table.sqliteDef.name, JSON.stringify(defaultValues)],
132
128
  }),
133
129
  [table, id, defaultValues, idStr],
134
130
  )
@@ -143,60 +139,10 @@ export const useClientDocument: {
143
139
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
144
140
  if (queryRef.valueRef.current === newValue) return
145
141
 
146
- store.commit(table.set(removeUndefinedValues(newValue), id as any))
142
+ store.commit(table.set(removeUndefinedValues(newValue), id))
147
143
  },
148
144
  [id, queryRef.valueRef, store, table],
149
145
  )
150
146
 
151
147
  return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
152
148
  }
153
-
154
- /**
155
- * A function that dispatches an action. Mirrors React's `Dispatch` type.
156
- * @typeParam A - The action type
157
- */
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
- */
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
- */
172
- export type SetStateAction<S> = S | ((previousValue: S) => S)
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
- */
182
- export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
183
- TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
184
- ? SetStateAction<TTableDef['Value']>
185
- : SetStateActionPartial<TTableDef['Value']>
186
- >
187
-
188
- const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
189
- if (State.SQLite.tableIsClientDocumentTable(table) === false) {
190
- return shouldNeverHappen(
191
- `useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
192
- )
193
- }
194
- }
195
-
196
- const removeUndefinedValues = (value: any) => {
197
- if (typeof value === 'object' && value !== null) {
198
- return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
199
- }
200
-
201
- return value
202
- }
@@ -1,14 +1,14 @@
1
+ import * as ReactTesting from '@testing-library/react'
2
+ import React from 'react'
3
+ import * as ReactWindow from 'react-window'
4
+ import { expect } from 'vitest'
5
+
1
6
  /** biome-ignore-all lint/a11y: test */
2
7
  import * as LiveStore from '@livestore/livestore'
3
8
  import { queryDb, StoreInternalsSymbol, signal } from '@livestore/livestore'
4
9
  import { RG } from '@livestore/livestore/internal/testing-utils'
5
- import { Effect, Schema } from '@livestore/utils/effect'
6
10
  import { Vitest } from '@livestore/utils-dev/node-vitest'
7
- import * as ReactTesting from '@testing-library/react'
8
- import React from 'react'
9
- // @ts-expect-error no types
10
- import * as ReactWindow from 'react-window'
11
- import { expect } from 'vitest'
11
+ import { Effect, Schema } from '@livestore/utils/effect'
12
12
 
13
13
  import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
14
14
  import { __resetUseRcResourceCache } from './useRcResource.ts'
@@ -156,7 +156,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
156
156
  width={100}
157
157
  itemSize={10}
158
158
  itemCount={numItems}
159
- itemData={Array.from({ length: numItems }, (_, i) => i).reverse()}
159
+ itemData={Array.from({ length: numItems }, (_, i) => i).toReversed()}
160
160
  >
161
161
  {ListItem}
162
162
  </ReactWindow.FixedSizeList>
@@ -223,7 +223,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
223
223
  const { result, rerender } = ReactTesting.renderHook(
224
224
  (useNum: boolean) => {
225
225
  renderCount.inc()
226
- const query$ = React.useMemo(() => (useNum ? num$ : str$), [useNum])
226
+ const query$ = React.useMemo(() => (useNum === true ? num$ : str$), [useNum])
227
227
  return store.useQuery(query$)
228
228
  },
229
229
  { wrapper, initialProps: false },
package/src/useQuery.ts CHANGED
@@ -1,22 +1,19 @@
1
- import { isQueryBuilder } from '@livestore/common'
2
- import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
1
+ import type * as otel from '@opentelemetry/api'
2
+ import React from 'react'
3
+
3
4
  import {
4
- extractStackInfoFromStackTrace,
5
- isQueryable,
6
- type Queryable,
7
- queryDb,
8
- type SignalDef,
9
- StoreInternalsSymbol,
10
- stackInfoToString,
11
- } from '@livestore/livestore'
5
+ captureStackInfo,
6
+ computeRcRefKey,
7
+ createQueryResource,
8
+ type NormalizedQueryable,
9
+ normalizeQueryable,
10
+ runInitialQuery,
11
+ } from '@livestore/framework-toolkit'
12
+ import type { LiveQuery, Queryable, Store } from '@livestore/livestore'
12
13
  import type { LiveQueries } from '@livestore/livestore/internal'
13
- import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
14
- import * as otel from '@opentelemetry/api'
15
- import React from 'react'
14
+ import { deepEqual, shouldNeverHappen } from '@livestore/utils'
16
15
 
17
- import { LiveStoreContext } from './LiveStoreContext.ts'
18
16
  import { useRcResource } from './useRcResource.ts'
19
- import { originalStackLimit } from './utils/stack-info.ts'
20
17
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.ts'
21
18
 
22
19
  /**
@@ -49,7 +46,7 @@ export const useQuery = <TQueryable extends Queryable<any>>(
49
46
  *
50
47
  * Parameters
51
48
  * - `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.
49
+ * - `options.store`: The store to use. Required when calling `useQueryRef` directly; automatically provided when using `store.useQuery()`.
53
50
  * - `options.otelContext`: Optional parent otel context for the query span.
54
51
  * - `options.otelSpanName`: Optional explicit span name; otherwise derived from the query label.
55
52
  *
@@ -71,130 +68,45 @@ export const useQueryRef = <TQueryable extends Queryable<any>>(
71
68
  valueRef: React.RefObject<Queryable.Result<TQueryable>>
72
69
  queryRcRef: LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>
73
70
  } => {
74
- const store =
75
- options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
76
- React.useContext(LiveStoreContext)?.store ??
77
- shouldNeverHappen(`No store provided to useQuery`)
71
+ const store = options?.store ?? shouldNeverHappen(`No store provided to useQuery`)
78
72
 
79
73
  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
74
 
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
-
103
- // It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
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])
75
+ const normalized = React.useMemo<NormalizedQueryable<TResult>>(
76
+ () => normalizeQueryable(queryable as Queryable<TResult>),
77
+ [queryable],
78
+ )
113
79
 
114
- const resourceLabel = normalized._tag === 'definition' ? normalized.def.label : normalized.query$.label
80
+ const rcRefKey = React.useMemo(() => computeRcRefKey(store, normalized), [normalized, store])
115
81
 
116
- const stackInfo = React.useMemo(() => {
117
- Error.stackTraceLimit = 10
118
- const stack = new Error().stack!
119
- Error.stackTraceLimit = originalStackLimit
120
- return extractStackInfoFromStackTrace(stack)
121
- }, [])
82
+ const stackInfo = React.useMemo(() => captureStackInfo(), [])
122
83
 
123
84
  const { queryRcRef, span, otelContext } = useRcResource(
124
85
  rcRefKey,
125
- () => {
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,
130
- )
131
-
132
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
133
-
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>>)
142
-
143
- return { queryRcRef, span, otelContext }
144
- },
86
+ () =>
87
+ createQueryResource(store, normalized, stackInfo, {
88
+ otelSpanName: options?.otelSpanName,
89
+ otelContext: options?.otelContext,
90
+ }),
145
91
  // We need to keep the queryRcRef alive a bit longer, so we have a second `useRcResource` below
146
92
  // which takes care of disposing the queryRcRef
147
93
  () => {},
148
94
  )
149
95
 
150
- // if (queryRcRef.value._tag === 'signal') {
151
- // const queryRcRef.value.get()
152
- // }
153
-
154
- const query$ = queryRcRef.value as LiveQuery<TResult>
96
+ const query$ = queryRcRef.value
155
97
 
156
98
  React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
157
- // console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
158
-
159
- const initialResult = React.useMemo(() => {
160
- try {
161
- return query$.run({
162
- otelContext,
163
- debugRefreshReason: {
164
- _tag: 'react',
165
- api: 'useQuery',
166
- label: `useQuery:initial-run:${query$.label}`,
167
- stackInfo,
168
- },
169
- })
170
- } catch (cause: any) {
171
- console.error('[@livestore/react:useQuery] Error running query', cause)
172
- throw new Error(
173
- `\
174
- [@livestore/react:useQuery] Error running query: ${cause.name}
175
99
 
176
- Query: ${query$.label}
177
-
178
- React trace:
179
-
180
- ${indent(stackInfoToString(stackInfo), 4)}
181
-
182
- Stack trace:
183
- `,
184
- { cause },
185
- )
186
- }
187
- }, [otelContext, query$, stackInfo])
100
+ const initialResult = React.useMemo(
101
+ () => runInitialQuery(query$, otelContext, stackInfo, 'react'),
102
+ [otelContext, query$, stackInfo],
103
+ )
188
104
 
189
105
  // We know the query has a result by the time we use it; so we can synchronously populate a default state
190
106
  const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
191
107
 
192
- // TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
193
- // before calling the LS `onEffect` on it
194
-
195
108
  // Subscribe to future updates for this query
196
109
  React.useEffect(() => {
197
- // TODO double check whether we still need `activeSubscriptions`
198
110
  query$.activeSubscriptions.add(stackInfo)
199
111
 
200
112
  // Dynamic queries only set their actual label after they've been run the first time,
@@ -225,7 +137,6 @@ Stack trace:
225
137
  rcRefKey,
226
138
  () => ({ queryRcRef, span }),
227
139
  ({ queryRcRef, span }) => {
228
- // console.debug('deref', queryRcRef.value.id, queryRcRef.value.label)
229
140
  queryRcRef.deref()
230
141
  span.end()
231
142
  },
@@ -9,7 +9,7 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
9
9
  __resetUseRcResourceCache()
10
10
  })
11
11
 
12
- const wrapper = strictMode ? React.StrictMode : React.Fragment
12
+ const wrapper = strictMode === true ? React.StrictMode : React.Fragment
13
13
 
14
14
  it('should create a stateful entity using make and call cleanup on unmount', () => {
15
15
  const makeSpy = vi.fn(() => Symbol('statefulResource'))
@@ -79,11 +79,16 @@ export const useRcResource = <T>(
79
79
  ): T => {
80
80
  const keyRef = React.useRef<string | undefined>(undefined)
81
81
  const didDisposeInMemo = React.useRef(false)
82
+ const createRef = React.useRef(create)
83
+ const disposeRef = React.useRef(dispose)
84
+
85
+ createRef.current = create
86
+ disposeRef.current = dispose
82
87
 
83
88
  // biome-ignore lint/correctness/useExhaustiveDependencies: Dependency is deliberately limited to `key` to avoid unintended re-creations.
84
89
  const resource = React.useMemo(() => {
85
90
  // console.debug('useMemo', key)
86
- if (didDisposeInMemo.current) {
91
+ if (didDisposeInMemo.current === true) {
87
92
  // console.debug('useMemo', key, 'skip')
88
93
  const cachedItem = cache.get(key)
89
94
  if (cachedItem !== undefined && cachedItem._tag === 'active') {
@@ -104,7 +109,7 @@ export const useRcResource = <T>(
104
109
 
105
110
  if (cachedItemForPreviousKey.rc === 0) {
106
111
  // Clean up the stateful resource if no longer referenced
107
- dispose(cachedItemForPreviousKey.resource)
112
+ disposeRef.current(cachedItemForPreviousKey.resource)
108
113
  cache.set(previousKey, { _tag: 'destroyed' })
109
114
  didDisposeInMemo.current = true
110
115
  }
@@ -122,7 +127,7 @@ export const useRcResource = <T>(
122
127
  }
123
128
 
124
129
  // Create a new stateful resource if not cached
125
- const resource = create()
130
+ const resource = createRef.current()
126
131
  cache.set(key, { _tag: 'active', rc: 1, resource })
127
132
  return resource
128
133
  }, [key])
@@ -130,7 +135,7 @@ export const useRcResource = <T>(
130
135
  // biome-ignore lint/correctness/useExhaustiveDependencies: We assume the `dispose` function is stable and won't change across renders
131
136
  React.useEffect(() => {
132
137
  return () => {
133
- if (didDisposeInMemo.current) {
138
+ if (didDisposeInMemo.current === true) {
134
139
  // console.debug('unmount', keyRef.current, 'skip')
135
140
  didDisposeInMemo.current = false
136
141
  return
@@ -146,7 +151,7 @@ export const useRcResource = <T>(
146
151
  // console.debug('rc--', cachedItem.rc, ...(_options?.debugPrint?.(cachedItem.resource) ?? []))
147
152
 
148
153
  if (cachedItem.rc === 0) {
149
- dispose(cachedItem.resource)
154
+ disposeRef.current(cachedItem.resource)
150
155
  cache.delete(key)
151
156
  }
152
157
  }