@livestore/react 0.3.0-dev.11 → 0.3.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/LiveStoreContext.d.ts +3 -5
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +3 -7
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +1 -2
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +20 -5
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +9 -6
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +7 -6
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +2 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +4 -5
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +1 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/useAtom.d.ts +2 -4
- package/dist/useAtom.d.ts.map +1 -1
- package/dist/useAtom.js +28 -32
- package/dist/useAtom.js.map +1 -1
- package/dist/useQuery.d.ts +3 -26
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +45 -60
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +16 -70
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRow.d.ts +7 -10
- package/dist/useRow.d.ts.map +1 -1
- package/dist/useRow.js +19 -16
- package/dist/useRow.js.map +1 -1
- package/dist/useRow.test.js +96 -74
- package/dist/useRow.test.js.map +1 -1
- package/dist/useScopedQuery.d.ts +4 -10
- package/dist/useScopedQuery.d.ts.map +1 -1
- package/dist/useScopedQuery.js +52 -97
- package/dist/useScopedQuery.js.map +1 -1
- package/dist/useScopedQuery.test.js +12 -13
- package/dist/useScopedQuery.test.js.map +1 -1
- package/dist/utils/useStateRefWithReactiveInput.d.ts +1 -1
- package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
- package/dist/utils/useStateRefWithReactiveInput.js.map +1 -1
- package/package.json +17 -18
- package/src/LiveStoreContext.ts +6 -10
- package/src/LiveStoreProvider.tsx +21 -7
- package/src/__snapshots__/useRow.test.tsx.snap +149 -337
- package/src/__tests__/fixture.tsx +10 -7
- package/src/experimental/components/LiveList.tsx +7 -8
- package/src/mod.ts +1 -0
- package/src/useAtom.ts +11 -22
- package/src/useQuery.test.tsx +67 -165
- package/src/useQuery.ts +54 -84
- package/src/useRow.test.tsx +163 -130
- package/src/useRow.ts +35 -32
- package/src/useScopedQuery.test.tsx +96 -0
- package/src/useScopedQuery.ts +142 -0
- package/src/utils/useStateRefWithReactiveInput.ts +1 -1
- package/dist/useRcRef.d.ts +0 -72
- package/dist/useRcRef.d.ts.map +0 -1
- package/dist/useRcRef.js +0 -146
- package/dist/useRcRef.js.map +0 -1
- package/dist/useRcRef.test.d.ts +0 -2
- package/dist/useRcRef.test.d.ts.map +0 -1
- package/dist/useRcRef.test.js +0 -128
- package/dist/useRcRef.test.js.map +0 -1
- package/dist/useRcResource.d.ts +0 -76
- package/dist/useRcResource.d.ts.map +0 -1
- package/dist/useRcResource.js +0 -150
- package/dist/useRcResource.js.map +0 -1
- package/dist/useRcResource.test.d.ts +0 -2
- package/dist/useRcResource.test.d.ts.map +0 -1
- package/dist/useRcResource.test.js +0 -122
- package/dist/useRcResource.test.js.map +0 -1
- package/src/__snapshots__/useQuery.test.tsx.snap +0 -2011
- package/src/useRcResource.test.tsx +0 -167
- package/src/useRcResource.ts +0 -180
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { provideOtel } from '@livestore/common'
|
|
2
2
|
import { DbSchema, makeSchema } from '@livestore/common/schema'
|
|
3
3
|
import type { LiveStoreContextRunning } from '@livestore/livestore'
|
|
4
|
-
import { createStore } from '@livestore/livestore'
|
|
4
|
+
import { createStore, globalReactivityGraph, makeReactivityGraph } from '@livestore/livestore'
|
|
5
5
|
import { Effect } from '@livestore/utils/effect'
|
|
6
6
|
import { makeInMemoryAdapter } from '@livestore/web'
|
|
7
7
|
import type * as otel from '@opentelemetry/api'
|
|
@@ -42,7 +42,7 @@ export const app = DbSchema.table(
|
|
|
42
42
|
{ isSingleton: true },
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
export const
|
|
45
|
+
export const AppComponentSchema = DbSchema.table(
|
|
46
46
|
'UserInfo',
|
|
47
47
|
{
|
|
48
48
|
username: DbSchema.text({ default: '' }),
|
|
@@ -59,16 +59,18 @@ export const AppRouterSchema = DbSchema.table(
|
|
|
59
59
|
{ isSingleton: true, deriveMutations: true },
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
export const tables = { todos, app,
|
|
62
|
+
export const tables = { todos, app, AppComponentSchema, AppRouterSchema }
|
|
63
63
|
export const schema = makeSchema({ tables })
|
|
64
64
|
|
|
65
65
|
export const makeTodoMvcReact = ({
|
|
66
66
|
otelTracer,
|
|
67
67
|
otelContext,
|
|
68
|
-
|
|
68
|
+
useGlobalReactivityGraph = true,
|
|
69
|
+
strictMode = process.env.REACT_STRICT_MODE !== undefined,
|
|
69
70
|
}: {
|
|
70
71
|
otelTracer?: otel.Tracer
|
|
71
72
|
otelContext?: otel.Context
|
|
73
|
+
useGlobalReactivityGraph?: boolean
|
|
72
74
|
strictMode?: boolean
|
|
73
75
|
} = {}) =>
|
|
74
76
|
Effect.gen(function* () {
|
|
@@ -87,10 +89,13 @@ export const makeTodoMvcReact = ({
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
|
|
93
|
+
|
|
90
94
|
const store = yield* createStore({
|
|
91
95
|
schema,
|
|
92
96
|
storeId: 'default',
|
|
93
97
|
adapter: makeInMemoryAdapter(),
|
|
98
|
+
reactivityGraph,
|
|
94
99
|
debug: { instanceId: 'test' },
|
|
95
100
|
})
|
|
96
101
|
|
|
@@ -107,7 +112,5 @@ export const makeTodoMvcReact = ({
|
|
|
107
112
|
</MaybeStrictMode>
|
|
108
113
|
)
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return { wrapper, store, renderCount }
|
|
115
|
+
return { wrapper, store, reactivityGraph, makeRenderCount, strictMode }
|
|
113
116
|
}).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer }))
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LiveQuery } from '@livestore/livestore'
|
|
2
2
|
import { computed } from '@livestore/livestore'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
|
|
5
5
|
import { useQuery } from '../../useQuery.js'
|
|
6
|
+
import { useScopedQuery } from '../../useScopedQuery.js'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
TODO:
|
|
@@ -11,7 +12,7 @@ TODO:
|
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
export type LiveListProps<TItem> = {
|
|
14
|
-
items$:
|
|
15
|
+
items$: LiveQuery<ReadonlyArray<TItem>>
|
|
15
16
|
// TODO refactor render-flag to allow for transition animations on add/remove
|
|
16
17
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
17
18
|
/** Needs to be unique across all list items */
|
|
@@ -31,16 +32,14 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
31
32
|
|
|
32
33
|
React.useEffect(() => setHasMounted(true), [])
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
+
const keysCb = React.useCallback(() => computed((get) => get(items$).map(getKey)), [getKey, items$])
|
|
36
|
+
const keys = useScopedQuery(keysCb, 'fixed')
|
|
35
37
|
const arr = React.useMemo(
|
|
36
38
|
() =>
|
|
37
39
|
keys.map(
|
|
38
40
|
(key) =>
|
|
39
41
|
// TODO figure out a way so that `item$` returns an ordered lookup map to more efficiently find the item by key
|
|
40
|
-
[
|
|
41
|
-
key,
|
|
42
|
-
computed((get) => get(items$).find((item) => getKey(item, 0) === key)!) as LiveQueryDef<TItem>,
|
|
43
|
-
] as const,
|
|
42
|
+
[key, computed((get) => get(items$).find((item) => getKey(item, 0) === key)!) as LiveQuery<TItem>] as const,
|
|
44
43
|
),
|
|
45
44
|
[getKey, items$, keys],
|
|
46
45
|
)
|
|
@@ -66,7 +65,7 @@ const ItemWrapper = <TItem,>({
|
|
|
66
65
|
renderItem,
|
|
67
66
|
}: {
|
|
68
67
|
itemKey: string | number
|
|
69
|
-
item$:
|
|
68
|
+
item$: LiveQuery<TItem>
|
|
70
69
|
opts: { index: number; isInitialListRender: boolean }
|
|
71
70
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
72
71
|
}) => {
|
package/src/mod.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { LiveStoreContext, useStore } from './LiveStoreContext.js'
|
|
2
2
|
export { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
3
3
|
export { useQuery } from './useQuery.js'
|
|
4
|
+
export { useScopedQuery } from './useScopedQuery.js'
|
|
4
5
|
export { useStackInfo } from './utils/stack-info.js'
|
|
5
6
|
export {
|
|
6
7
|
useRow,
|
package/src/useAtom.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { DerivedMutationHelperFns, QueryInfo } from '@livestore/common'
|
|
2
2
|
import type { DbSchema } from '@livestore/common/schema'
|
|
3
3
|
import type { SqliteDsl } from '@livestore/db-schema'
|
|
4
|
-
import type {
|
|
5
|
-
import { shouldNeverHappen } from '@livestore/utils'
|
|
4
|
+
import type { LiveQuery } from '@livestore/livestore'
|
|
6
5
|
import React from 'react'
|
|
7
6
|
|
|
8
7
|
import { useStore } from './LiveStoreContext.js'
|
|
@@ -11,27 +10,18 @@ import type { Dispatch, SetStateAction } from './useRow.js'
|
|
|
11
10
|
|
|
12
11
|
export const useAtom = <
|
|
13
12
|
// TODO also support colJsonValue
|
|
14
|
-
TQuery extends
|
|
13
|
+
TQuery extends LiveQuery<any, QueryInfo.Row | QueryInfo.Col>,
|
|
15
14
|
>(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
): [value: GetResult<TQuery>, setValue: Dispatch<SetStateAction<Partial<GetResult<TQuery>>>>] => {
|
|
21
|
-
const queryRef = useQueryRef(queryDef, { store: options?.store })
|
|
22
|
-
const query$ = queryRef.queryRcRef.value
|
|
23
|
-
|
|
24
|
-
// @ts-expect-error runtime check
|
|
25
|
-
if (query$.queryInfo._tag === 'None') {
|
|
26
|
-
shouldNeverHappen(`Can't useAtom with a query that has no queryInfo`, queryDef)
|
|
27
|
-
}
|
|
15
|
+
query$: TQuery,
|
|
16
|
+
): [value: TQuery['__result!'], setValue: Dispatch<SetStateAction<Partial<TQuery['__result!']>>>] => {
|
|
17
|
+
const query$Ref = useQueryRef(query$)
|
|
28
18
|
|
|
29
19
|
const { store } = useStore()
|
|
30
20
|
|
|
31
21
|
// TODO make API equivalent to useRow
|
|
32
|
-
const setValue = React.useMemo<Dispatch<SetStateAction<
|
|
33
|
-
|
|
34
|
-
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(
|
|
22
|
+
const setValue = React.useMemo<Dispatch<SetStateAction<TQuery['__result!']>>>(() => {
|
|
23
|
+
return (newValueOrFn: any) => {
|
|
24
|
+
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
|
|
35
25
|
const table = query$.queryInfo.table as DbSchema.TableDef &
|
|
36
26
|
DerivedMutationHelperFns<SqliteDsl.Columns, DbSchema.TableOptions>
|
|
37
27
|
|
|
@@ -55,9 +45,8 @@ export const useAtom = <
|
|
|
55
45
|
)
|
|
56
46
|
}
|
|
57
47
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
)
|
|
48
|
+
}
|
|
49
|
+
}, [query$.queryInfo, query$Ref, store])
|
|
61
50
|
|
|
62
|
-
return [
|
|
51
|
+
return [query$Ref.current, setValue]
|
|
63
52
|
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -1,180 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as LiveStore from '@livestore/livestore'
|
|
3
|
-
import { RG } from '@livestore/livestore/internal/testing-utils'
|
|
1
|
+
import { queryDb } from '@livestore/livestore'
|
|
4
2
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
5
|
-
import {
|
|
6
|
-
import * as ReactTesting from '@testing-library/react'
|
|
3
|
+
import { renderHook } from '@testing-library/react'
|
|
7
4
|
import React from 'react'
|
|
8
|
-
|
|
9
|
-
import * as ReactWindow from 'react-window'
|
|
10
|
-
import { expect } from 'vitest'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
11
6
|
|
|
12
7
|
import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
13
8
|
import * as LiveStoreReact from './mod.js'
|
|
14
|
-
import { __resetUseRcResourceCache } from './useRcResource.js'
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
'
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
RG.__resetIds()
|
|
21
|
-
__resetUseRcResourceCache()
|
|
22
|
-
})
|
|
10
|
+
describe('useQuery', () => {
|
|
11
|
+
it('simple', () =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
Effect.gen(function* () {
|
|
26
|
-
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
|
|
15
|
+
const renderCount = makeRenderCount()
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.schema) })
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
const { result } = renderHook(
|
|
20
|
+
() => {
|
|
21
|
+
renderCount.inc()
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
return LiveStoreReact.useQuery(allTodos$)
|
|
24
|
+
},
|
|
25
|
+
{ wrapper },
|
|
26
|
+
)
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
28
|
+
expect(result.current.length).toBe(0)
|
|
29
|
+
expect(renderCount.val).toBe(1)
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
React.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
|
|
44
32
|
|
|
45
|
-
|
|
33
|
+
expect(result.current.length).toBe(1)
|
|
34
|
+
expect(result.current[0]!.text).toBe('buy milk')
|
|
35
|
+
expect(renderCount.val).toBe(2)
|
|
36
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
it('same `useQuery` hook invoked with different queries', () =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(result.current).toBe('buy soy milk')
|
|
92
|
-
expect(renderCount.val).toBe(2)
|
|
93
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('2: after first mutation')
|
|
94
|
-
|
|
95
|
-
rerender('t2')
|
|
96
|
-
|
|
97
|
-
expect(result.current).toBe('buy eggs')
|
|
98
|
-
expect(renderCount.val).toBe(3)
|
|
99
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('3: after forced rerender')
|
|
100
|
-
}),
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
Vitest.scopedLive('filtered dependency query', () =>
|
|
104
|
-
Effect.gen(function* () {
|
|
105
|
-
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
|
|
106
|
-
|
|
107
|
-
const filter$ = makeRef('t1', { label: 'id-filter' })
|
|
108
|
-
|
|
109
|
-
const todo$ = queryDb((get) => tables.todos.query.where('id', get(filter$)), { label: 'todo' })
|
|
110
|
-
|
|
111
|
-
store.mutate(
|
|
112
|
-
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
113
|
-
todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
const { result } = ReactTesting.renderHook(
|
|
117
|
-
() => {
|
|
118
|
-
renderCount.inc()
|
|
119
|
-
|
|
120
|
-
return LiveStoreReact.useQuery(todo$)[0]!.text
|
|
121
|
-
},
|
|
122
|
-
{ wrapper },
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
expect(result.current).toBe('buy milk')
|
|
126
|
-
expect(renderCount.val).toBe(1)
|
|
127
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
128
|
-
|
|
129
|
-
ReactTesting.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
|
|
130
|
-
|
|
131
|
-
expect(result.current).toBe('buy soy milk')
|
|
132
|
-
expect(renderCount.val).toBe(2)
|
|
133
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
134
|
-
|
|
135
|
-
ReactTesting.act(() => store.setRef(filter$, 't2'))
|
|
136
|
-
|
|
137
|
-
expect(result.current).toBe('buy eggs')
|
|
138
|
-
expect(renderCount.val).toBe(3)
|
|
139
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
140
|
-
}),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
// NOTE this test covers some special react lifecyle paths which I couldn't easily reproduce without react-window
|
|
144
|
-
// it basically causes a "query swap" in the `useMemo` and both a `useEffect` cleanup call.
|
|
145
|
-
// To handle this properly we introduced the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`.
|
|
146
|
-
Vitest.scopedLive('should work for a list with react-window', () =>
|
|
147
|
-
Effect.gen(function* () {
|
|
148
|
-
const { wrapper } = yield* makeTodoMvcReact({ strictMode })
|
|
149
|
-
|
|
150
|
-
const ListWrapper: React.FC<{ numItems: number }> = ({ numItems }) => {
|
|
151
|
-
return (
|
|
152
|
-
<ReactWindow.FixedSizeList
|
|
153
|
-
height={100}
|
|
154
|
-
width={100}
|
|
155
|
-
itemSize={10}
|
|
156
|
-
itemCount={numItems}
|
|
157
|
-
itemData={Array.from({ length: numItems }, (_, i) => i).reverse()}
|
|
158
|
-
>
|
|
159
|
-
{ListItem}
|
|
160
|
-
</ReactWindow.FixedSizeList>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const ListItem: React.FC<{ data: ReadonlyArray<number>; index: number }> = ({ data: ids, index }) => {
|
|
165
|
-
const id = ids[index]!
|
|
166
|
-
const res = LiveStoreReact.useQuery(LiveStore.computed(() => id, { label: `ListItem.${id}`, deps: id }))
|
|
167
|
-
return <div role="listitem">{res}</div>
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const renderResult = ReactTesting.render(<ListWrapper numItems={1} />, { wrapper })
|
|
171
|
-
|
|
172
|
-
expect(renderResult.container.textContent).toBe('0')
|
|
173
|
-
|
|
174
|
-
renderResult.rerender(<ListWrapper numItems={2} />)
|
|
175
|
-
|
|
176
|
-
expect(renderResult.container.textContent).toBe('10')
|
|
177
|
-
}),
|
|
178
|
-
)
|
|
179
|
-
},
|
|
180
|
-
)
|
|
42
|
+
const renderCount = makeRenderCount()
|
|
43
|
+
|
|
44
|
+
const todo1$ = queryDb(
|
|
45
|
+
{ query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.schema) },
|
|
46
|
+
{ label: 'libraryTracksView1' },
|
|
47
|
+
)
|
|
48
|
+
const todo2$ = queryDb(
|
|
49
|
+
{ query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.schema) },
|
|
50
|
+
{ label: 'libraryTracksView2' },
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
store.mutate(
|
|
54
|
+
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
55
|
+
todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const { result, rerender } = renderHook(
|
|
59
|
+
(todoId: string) => {
|
|
60
|
+
renderCount.inc()
|
|
61
|
+
|
|
62
|
+
const query$ = React.useMemo(() => (todoId === 't1' ? todo1$ : todo2$), [todoId])
|
|
63
|
+
|
|
64
|
+
return LiveStoreReact.useQuery(query$)[0]!.text
|
|
65
|
+
},
|
|
66
|
+
{ wrapper, initialProps: 't1' },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(result.current).toBe('buy milk')
|
|
70
|
+
expect(renderCount.val).toBe(1)
|
|
71
|
+
|
|
72
|
+
React.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
|
|
73
|
+
|
|
74
|
+
expect(result.current).toBe('buy soy milk')
|
|
75
|
+
expect(renderCount.val).toBe(2)
|
|
76
|
+
|
|
77
|
+
rerender('t2')
|
|
78
|
+
|
|
79
|
+
expect(result.current).toBe('buy eggs')
|
|
80
|
+
expect(renderCount.val).toBe(3)
|
|
81
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
82
|
+
})
|
package/src/useQuery.ts
CHANGED
|
@@ -1,51 +1,38 @@
|
|
|
1
|
-
import type { GetResult,
|
|
1
|
+
import type { GetResult, LiveQueryAny } from '@livestore/livestore'
|
|
2
2
|
import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
|
|
3
3
|
import { deepEqual, indent } from '@livestore/utils'
|
|
4
4
|
import * as otel from '@opentelemetry/api'
|
|
5
5
|
import React from 'react'
|
|
6
6
|
|
|
7
7
|
import { useStore } from './LiveStoreContext.js'
|
|
8
|
-
import { useRcResource } from './useRcResource.js'
|
|
9
8
|
import { originalStackLimit } from './utils/stack-info.js'
|
|
10
9
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Example:
|
|
16
|
-
* ```tsx
|
|
17
|
-
* const App = () => {
|
|
18
|
-
* const todos = useQuery(queryDb(tables.todos.query.where({ complete: true })))
|
|
19
|
-
* return <div>{todos.map((todo) => <div key={todo.id}>{todo.title}</div>)}</div>
|
|
20
|
-
* }
|
|
21
|
-
* ```
|
|
12
|
+
* NOTE Some folks have suggested to use `React.useSyncExternalStore`, however, it's not doing anything special
|
|
13
|
+
* for what's needed here, so we handle everything manually.
|
|
22
14
|
*/
|
|
23
|
-
export const useQuery = <TQuery extends LiveQueryDefAny>(
|
|
24
|
-
queryDef: TQuery,
|
|
25
|
-
options?: { store?: Store },
|
|
26
|
-
): GetResult<TQuery> => useQueryRef(queryDef, options).valueRef.current
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
/**
|
|
17
|
+
* This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
|
|
18
|
+
* so we need to "cache" the fact that we've already started a span for this component.
|
|
19
|
+
* The map entry is being removed again in the `React.useEffect` call below.
|
|
20
|
+
*/
|
|
21
|
+
const spanAlreadyStartedCache = new Map<LiveQueryAny, { span: otel.Span; otelContext: otel.Context }>()
|
|
22
|
+
|
|
23
|
+
export const useQuery = <TQuery extends LiveQueryAny>(query: TQuery): GetResult<TQuery> => useQueryRef(query).current
|
|
30
24
|
|
|
31
25
|
/**
|
|
26
|
+
*
|
|
32
27
|
*/
|
|
33
|
-
export const useQueryRef = <TQuery extends
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
): {
|
|
43
|
-
valueRef: React.RefObject<GetResult<TQuery>>
|
|
44
|
-
queryRcRef: RcRef<LiveQuery<GetResult<TQuery>, GetQueryInfo<TQuery>>>
|
|
45
|
-
} => {
|
|
46
|
-
const { store } = useStore({ store: options?.store })
|
|
47
|
-
|
|
48
|
-
const rcRefKey = `${store.storeId}_${queryDef.hash}`
|
|
28
|
+
export const useQueryRef = <TQuery extends LiveQueryAny>(
|
|
29
|
+
query$: TQuery,
|
|
30
|
+
parentOtelContext?: otel.Context,
|
|
31
|
+
): React.MutableRefObject<GetResult<TQuery>> => {
|
|
32
|
+
const { store } = useStore()
|
|
33
|
+
|
|
34
|
+
React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
35
|
+
// console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
49
36
|
|
|
50
37
|
const stackInfo = React.useMemo(() => {
|
|
51
38
|
Error.stackTraceLimit = 10
|
|
@@ -55,42 +42,31 @@ export const useQueryRef = <TQuery extends LiveQueryDefAny>(
|
|
|
55
42
|
return extractStackInfoFromStackTrace(stack)
|
|
56
43
|
}, [])
|
|
57
44
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const span = store.otel.tracer.startSpan(
|
|
64
|
-
options?.otelSpanName ?? `LiveStore:useQuery:${queryDefLabel}`,
|
|
65
|
-
{ attributes: { label: queryDefLabel, firstStackInfo: JSON.stringify(stackInfo) } },
|
|
66
|
-
options?.otelContext ?? store.otel.queriesSpanContext,
|
|
67
|
-
)
|
|
45
|
+
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
46
|
+
const { span, otelContext } = React.useMemo(() => {
|
|
47
|
+
const existingSpan = spanAlreadyStartedCache.get(query$)
|
|
48
|
+
if (existingSpan !== undefined) return existingSpan
|
|
68
49
|
|
|
69
|
-
|
|
50
|
+
const span = store.otel.tracer.startSpan(
|
|
51
|
+
`LiveStore:useQuery:${query$.label}`,
|
|
52
|
+
{ attributes: { label: query$.label, stackInfo: JSON.stringify(stackInfo) } },
|
|
53
|
+
parentOtelContext ?? store.otel.queriesSpanContext,
|
|
54
|
+
)
|
|
70
55
|
|
|
71
|
-
|
|
56
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
// We need to keep the queryRcRef alive a bit longer, so we have a second `useRcResource` below
|
|
76
|
-
// which takes care of disposing the queryRcRef
|
|
77
|
-
() => {},
|
|
78
|
-
)
|
|
79
|
-
const query$ = queryRcRef.value as LiveQuery<GetResult<TQuery>, GetQueryInfo<TQuery>>
|
|
58
|
+
spanAlreadyStartedCache.set(query$, { span, otelContext })
|
|
80
59
|
|
|
81
|
-
|
|
82
|
-
|
|
60
|
+
return { span, otelContext }
|
|
61
|
+
}, [parentOtelContext, query$, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
83
62
|
|
|
84
63
|
const initialResult = React.useMemo(() => {
|
|
85
64
|
try {
|
|
86
|
-
return query$.run({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
label: `useQuery:initial-run:${query$.label}`,
|
|
92
|
-
stackInfo,
|
|
93
|
-
},
|
|
65
|
+
return query$.run(otelContext, {
|
|
66
|
+
_tag: 'react',
|
|
67
|
+
api: 'useQuery',
|
|
68
|
+
label: query$.label,
|
|
69
|
+
stackInfo,
|
|
94
70
|
})
|
|
95
71
|
} catch (cause: any) {
|
|
96
72
|
throw new Error(
|
|
@@ -113,20 +89,25 @@ Stack trace:
|
|
|
113
89
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
114
90
|
const [valueRef, setValue] = useStateRefWithReactiveInput<GetResult<TQuery>>(initialResult)
|
|
115
91
|
|
|
116
|
-
|
|
117
|
-
|
|
92
|
+
React.useEffect(
|
|
93
|
+
() => () => {
|
|
94
|
+
spanAlreadyStartedCache.delete(query$)
|
|
95
|
+
span.end()
|
|
96
|
+
},
|
|
97
|
+
[query$, span],
|
|
98
|
+
)
|
|
118
99
|
|
|
119
100
|
// Subscribe to future updates for this query
|
|
120
101
|
React.useEffect(() => {
|
|
121
|
-
// TODO double check whether we still need `activeSubscriptions`
|
|
122
102
|
query$.activeSubscriptions.add(stackInfo)
|
|
123
103
|
|
|
124
104
|
// Dynamic queries only set their actual label after they've been run the first time,
|
|
125
105
|
// so we're also updating the span name here.
|
|
126
|
-
span.updateName(
|
|
106
|
+
span.updateName(`LiveStore:useQuery:${query$.label}`)
|
|
127
107
|
|
|
128
|
-
return store.subscribe(
|
|
129
|
-
|
|
108
|
+
return store.subscribe(
|
|
109
|
+
query$,
|
|
110
|
+
(newValue) => {
|
|
130
111
|
// NOTE: we return a reference to the result object within LiveStore;
|
|
131
112
|
// this implies that app code must not mutate the results, or else
|
|
132
113
|
// there may be weird reactivity bugs.
|
|
@@ -134,23 +115,12 @@ Stack trace:
|
|
|
134
115
|
setValue(newValue)
|
|
135
116
|
}
|
|
136
117
|
},
|
|
137
|
-
|
|
118
|
+
() => {
|
|
138
119
|
query$.activeSubscriptions.delete(stackInfo)
|
|
139
120
|
},
|
|
140
|
-
label: query$.label,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}, [stackInfo, query$, setValue, store, valueRef, otelContext, span, options?.otelSpanName])
|
|
144
|
-
|
|
145
|
-
useRcResource(
|
|
146
|
-
rcRefKey,
|
|
147
|
-
() => ({ queryRcRef, span }),
|
|
148
|
-
({ queryRcRef, span }) => {
|
|
149
|
-
// console.debug('deref', queryRcRef.value.id, queryRcRef.value.label)
|
|
150
|
-
queryRcRef.deref()
|
|
151
|
-
span.end()
|
|
152
|
-
},
|
|
153
|
-
)
|
|
121
|
+
{ label: query$.label, otelContext },
|
|
122
|
+
)
|
|
123
|
+
}, [stackInfo, query$, setValue, store, valueRef, otelContext, span])
|
|
154
124
|
|
|
155
|
-
return
|
|
125
|
+
return valueRef
|
|
156
126
|
}
|