@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.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +8 -280
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +9 -84
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +9 -7
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/experimental/mod.d.ts +0 -1
- package/dist/experimental/mod.d.ts.map +1 -1
- package/dist/experimental/mod.js +0 -1
- package/dist/experimental/mod.js.map +1 -1
- package/dist/mod.d.ts +8 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +6 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +1 -26
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +3 -17
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +12 -4
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +4 -5
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +12 -85
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +7 -8
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +9 -5
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +1 -1
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +61 -46
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +75 -60
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/{experimental/multi-store/useStore.test.js → useStore.test.js} +70 -27
- package/dist/useStore.test.js.map +1 -0
- package/dist/useSyncStatus.d.ts +22 -0
- package/dist/useSyncStatus.d.ts.map +1 -0
- package/dist/useSyncStatus.js +28 -0
- package/dist/useSyncStatus.js.map +1 -0
- package/package.json +69 -26
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.test.tsx.snap +112 -78
- package/src/__tests__/fixture.tsx +23 -118
- package/src/experimental/components/LiveList.tsx +22 -9
- package/src/experimental/mod.ts +0 -1
- package/src/mod.ts +8 -12
- package/src/useClientDocument.test.tsx +16 -6
- package/src/useClientDocument.ts +7 -61
- package/src/useQuery.test.tsx +8 -8
- package/src/useQuery.ts +30 -119
- package/src/useRcResource.test.tsx +1 -1
- package/src/useRcResource.ts +10 -5
- package/src/{experimental/multi-store/useStore.test.tsx → useStore.test.tsx} +117 -39
- package/src/useStore.ts +106 -65
- package/src/useSyncStatus.ts +34 -0
- package/dist/LiveStoreContext.d.ts +0 -40
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -21
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -73
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -233
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +0 -105
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.js +0 -184
- package/dist/experimental/multi-store/StoreRegistry.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts +0 -2
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistry.test.js +0 -381
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +0 -1
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts +0 -10
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +0 -1
- package/dist/experimental/multi-store/StoreRegistryContext.js +0 -15
- package/dist/experimental/multi-store/StoreRegistryContext.js.map +0 -1
- package/dist/experimental/multi-store/mod.d.ts +0 -6
- package/dist/experimental/multi-store/mod.d.ts.map +0 -1
- package/dist/experimental/multi-store/mod.js +0 -6
- package/dist/experimental/multi-store/mod.js.map +0 -1
- package/dist/experimental/multi-store/storeOptions.d.ts +0 -4
- package/dist/experimental/multi-store/storeOptions.d.ts.map +0 -1
- package/dist/experimental/multi-store/storeOptions.js +0 -4
- package/dist/experimental/multi-store/storeOptions.js.map +0 -1
- package/dist/experimental/multi-store/types.d.ts +0 -25
- package/dist/experimental/multi-store/types.d.ts.map +0 -1
- package/dist/experimental/multi-store/types.js +0 -2
- package/dist/experimental/multi-store/types.js.map +0 -1
- package/dist/experimental/multi-store/useStore.d.ts +0 -11
- package/dist/experimental/multi-store/useStore.d.ts.map +0 -1
- package/dist/experimental/multi-store/useStore.js +0 -16
- package/dist/experimental/multi-store/useStore.js.map +0 -1
- package/dist/experimental/multi-store/useStore.test.d.ts.map +0 -1
- package/dist/experimental/multi-store/useStore.test.js.map +0 -1
- package/dist/utils/stack-info.d.ts +0 -4
- package/dist/utils/stack-info.d.ts.map +0 -1
- package/dist/utils/stack-info.js +0 -10
- package/dist/utils/stack-info.js.map +0 -1
- package/src/LiveStoreContext.ts +0 -41
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -430
- package/src/ambient.d.ts +0 -1
- package/src/experimental/multi-store/StoreRegistry.test.ts +0 -518
- package/src/experimental/multi-store/StoreRegistry.ts +0 -253
- package/src/experimental/multi-store/StoreRegistryContext.tsx +0 -23
- package/src/experimental/multi-store/mod.ts +0 -5
- package/src/experimental/multi-store/storeOptions.ts +0 -8
- package/src/experimental/multi-store/types.ts +0 -37
- package/src/experimental/multi-store/useStore.ts +0 -26
- package/src/utils/stack-info.ts +0 -13
- /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(
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
index,
|
|
75
|
+
isInitialListRender,
|
|
68
76
|
renderItem,
|
|
77
|
+
store,
|
|
69
78
|
}: {
|
|
70
79
|
itemKey: string | number
|
|
71
80
|
item$: LiveQueryDef<TItem>
|
|
72
|
-
|
|
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.
|
|
86
|
-
prev.
|
|
97
|
+
prev.store === next.store &&
|
|
98
|
+
prev.index === next.index &&
|
|
99
|
+
prev.isInitialListRender === next.isInitialListRender,
|
|
87
100
|
) as typeof ItemWrapper
|
package/src/experimental/mod.ts
CHANGED
package/src/mod.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
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
|
|
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={
|
|
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}
|
|
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__'
|
package/src/useClientDocument.ts
CHANGED
|
@@ -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
|
|
131
|
-
deps: [idStr
|
|
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
|
|
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
|
-
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -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
|
|
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).
|
|
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
|
|
2
|
-
import
|
|
1
|
+
import type * as otel from '@opentelemetry/api'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|
|
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`:
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
127
|
-
options?.otelSpanName
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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'))
|
package/src/useRcResource.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
154
|
+
disposeRef.current(cachedItem.resource)
|
|
150
155
|
cache.delete(key)
|
|
151
156
|
}
|
|
152
157
|
}
|