@livestore/react 0.3.0-dev.10 → 0.3.0-dev.12
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 +5 -3
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +7 -3
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +5 -2
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +2 -17
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +6 -8
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +6 -7
- 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 +5 -4
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +0 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +0 -1
- package/dist/mod.js.map +1 -1
- package/dist/useAtom.d.ts +4 -2
- package/dist/useAtom.d.ts.map +1 -1
- package/dist/useAtom.js +32 -28
- package/dist/useAtom.js.map +1 -1
- package/dist/useQuery.d.ts +26 -3
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +60 -45
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +70 -16
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcRef.d.ts +72 -0
- package/dist/useRcRef.d.ts.map +1 -0
- package/dist/useRcRef.js +146 -0
- package/dist/useRcRef.js.map +1 -0
- package/dist/useRcRef.test.d.ts +2 -0
- package/dist/useRcRef.test.d.ts.map +1 -0
- package/dist/useRcRef.test.js +128 -0
- package/dist/useRcRef.test.js.map +1 -0
- package/dist/useRcResource.d.ts +76 -0
- package/dist/useRcResource.d.ts.map +1 -0
- package/dist/useRcResource.js +150 -0
- package/dist/useRcResource.js.map +1 -0
- package/dist/useRcResource.test.d.ts +2 -0
- package/dist/useRcResource.test.d.ts.map +1 -0
- package/dist/useRcResource.test.js +122 -0
- package/dist/useRcResource.test.js.map +1 -0
- package/dist/useRow.d.ts +10 -7
- package/dist/useRow.d.ts.map +1 -1
- package/dist/useRow.js +23 -22
- package/dist/useRow.js.map +1 -1
- package/dist/useRow.test.js +62 -80
- package/dist/useRow.test.js.map +1 -1
- package/dist/useScopedQuery.d.ts +10 -4
- package/dist/useScopedQuery.d.ts.map +1 -1
- package/dist/useScopedQuery.js +96 -52
- package/dist/useScopedQuery.js.map +1 -1
- package/dist/useScopedQuery.test.js +13 -12
- package/dist/useScopedQuery.test.js.map +1 -1
- package/package.json +6 -6
- package/src/LiveStoreContext.ts +10 -6
- package/src/LiveStoreProvider.tsx +3 -19
- package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
- package/src/__snapshots__/useRow.test.tsx.snap +335 -142
- package/src/__tests__/fixture.tsx +6 -9
- package/src/experimental/components/LiveList.tsx +8 -7
- package/src/mod.ts +0 -1
- package/src/useAtom.ts +22 -11
- package/src/useQuery.test.tsx +165 -67
- package/src/useQuery.ts +84 -54
- package/src/useRcResource.test.tsx +167 -0
- package/src/useRcResource.ts +180 -0
- package/src/useRow.test.tsx +73 -107
- package/src/useRow.ts +42 -40
- package/src/useScopedQuery.test.tsx +0 -96
- package/src/useScopedQuery.ts +0 -143
|
@@ -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
|
|
4
|
+
import { createStore } 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 userInfo = DbSchema.table(
|
|
46
46
|
'UserInfo',
|
|
47
47
|
{
|
|
48
48
|
username: DbSchema.text({ default: '' }),
|
|
@@ -59,18 +59,16 @@ 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, userInfo, AppRouterSchema }
|
|
63
63
|
export const schema = makeSchema({ tables })
|
|
64
64
|
|
|
65
65
|
export const makeTodoMvcReact = ({
|
|
66
66
|
otelTracer,
|
|
67
67
|
otelContext,
|
|
68
|
-
useGlobalReactivityGraph = true,
|
|
69
68
|
strictMode,
|
|
70
69
|
}: {
|
|
71
70
|
otelTracer?: otel.Tracer
|
|
72
71
|
otelContext?: otel.Context
|
|
73
|
-
useGlobalReactivityGraph?: boolean
|
|
74
72
|
strictMode?: boolean
|
|
75
73
|
} = {}) =>
|
|
76
74
|
Effect.gen(function* () {
|
|
@@ -89,13 +87,10 @@ export const makeTodoMvcReact = ({
|
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
const reactivityGraph = useGlobalReactivityGraph ? globalReactivityGraph : makeReactivityGraph()
|
|
93
|
-
|
|
94
90
|
const store = yield* createStore({
|
|
95
91
|
schema,
|
|
96
92
|
storeId: 'default',
|
|
97
93
|
adapter: makeInMemoryAdapter(),
|
|
98
|
-
reactivityGraph,
|
|
99
94
|
debug: { instanceId: 'test' },
|
|
100
95
|
})
|
|
101
96
|
|
|
@@ -112,5 +107,7 @@ export const makeTodoMvcReact = ({
|
|
|
112
107
|
</MaybeStrictMode>
|
|
113
108
|
)
|
|
114
109
|
|
|
115
|
-
|
|
110
|
+
const renderCount = makeRenderCount()
|
|
111
|
+
|
|
112
|
+
return { wrapper, store, renderCount }
|
|
116
113
|
}).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer }))
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LiveQueryDef } 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'
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
TODO:
|
|
@@ -12,7 +11,7 @@ TODO:
|
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
export type LiveListProps<TItem> = {
|
|
15
|
-
items$:
|
|
14
|
+
items$: LiveQueryDef<ReadonlyArray<TItem>>
|
|
16
15
|
// TODO refactor render-flag to allow for transition animations on add/remove
|
|
17
16
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
18
17
|
/** Needs to be unique across all list items */
|
|
@@ -32,14 +31,16 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
32
31
|
|
|
33
32
|
React.useEffect(() => setHasMounted(true), [])
|
|
34
33
|
|
|
35
|
-
const
|
|
36
|
-
const keys = useScopedQuery(keysCb, 'fixed')
|
|
34
|
+
const keys = useQuery(computed((get) => get(items$).map(getKey)))
|
|
37
35
|
const arr = React.useMemo(
|
|
38
36
|
() =>
|
|
39
37
|
keys.map(
|
|
40
38
|
(key) =>
|
|
41
39
|
// TODO figure out a way so that `item$` returns an ordered lookup map to more efficiently find the item by key
|
|
42
|
-
[
|
|
40
|
+
[
|
|
41
|
+
key,
|
|
42
|
+
computed((get) => get(items$).find((item) => getKey(item, 0) === key)!) as LiveQueryDef<TItem>,
|
|
43
|
+
] as const,
|
|
43
44
|
),
|
|
44
45
|
[getKey, items$, keys],
|
|
45
46
|
)
|
|
@@ -65,7 +66,7 @@ const ItemWrapper = <TItem,>({
|
|
|
65
66
|
renderItem,
|
|
66
67
|
}: {
|
|
67
68
|
itemKey: string | number
|
|
68
|
-
item$:
|
|
69
|
+
item$: LiveQueryDef<TItem>
|
|
69
70
|
opts: { index: number; isInitialListRender: boolean }
|
|
70
71
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
71
72
|
}) => {
|
package/src/mod.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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'
|
|
5
4
|
export { useStackInfo } from './utils/stack-info.js'
|
|
6
5
|
export {
|
|
7
6
|
useRow,
|
package/src/useAtom.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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 {
|
|
4
|
+
import type { GetResult, LiveQueryDef, Store } from '@livestore/livestore'
|
|
5
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
|
5
6
|
import React from 'react'
|
|
6
7
|
|
|
7
8
|
import { useStore } from './LiveStoreContext.js'
|
|
@@ -10,18 +11,27 @@ import type { Dispatch, SetStateAction } from './useRow.js'
|
|
|
10
11
|
|
|
11
12
|
export const useAtom = <
|
|
12
13
|
// TODO also support colJsonValue
|
|
13
|
-
TQuery extends
|
|
14
|
+
TQuery extends LiveQueryDef<any, QueryInfo.Row | QueryInfo.Col>,
|
|
14
15
|
>(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
queryDef: TQuery,
|
|
17
|
+
options?: {
|
|
18
|
+
store?: Store
|
|
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
|
+
}
|
|
18
28
|
|
|
19
29
|
const { store } = useStore()
|
|
20
30
|
|
|
21
31
|
// TODO make API equivalent to useRow
|
|
22
|
-
const setValue = React.useMemo<Dispatch<SetStateAction<TQuery
|
|
23
|
-
|
|
24
|
-
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(
|
|
32
|
+
const setValue = React.useMemo<Dispatch<SetStateAction<Partial<GetResult<TQuery>>>>>(
|
|
33
|
+
() => (newValueOrFn: any) => {
|
|
34
|
+
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
|
|
25
35
|
const table = query$.queryInfo.table as DbSchema.TableDef &
|
|
26
36
|
DerivedMutationHelperFns<SqliteDsl.Columns, DbSchema.TableOptions>
|
|
27
37
|
|
|
@@ -45,8 +55,9 @@ export const useAtom = <
|
|
|
45
55
|
)
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
58
|
+
},
|
|
59
|
+
[query$.queryInfo, queryRef.valueRef, store],
|
|
60
|
+
)
|
|
50
61
|
|
|
51
|
-
return [
|
|
62
|
+
return [queryRef.valueRef.current, setValue]
|
|
52
63
|
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -1,82 +1,180 @@
|
|
|
1
|
-
import { queryDb } from '@livestore/livestore'
|
|
1
|
+
import { makeRef, queryDb } from '@livestore/livestore'
|
|
2
|
+
import * as LiveStore from '@livestore/livestore'
|
|
3
|
+
import { RG } from '@livestore/livestore/internal/testing-utils'
|
|
2
4
|
import { Effect, Schema } from '@livestore/utils/effect'
|
|
3
|
-
import {
|
|
5
|
+
import { Vitest } from '@livestore/utils/node-vitest'
|
|
6
|
+
import * as ReactTesting from '@testing-library/react'
|
|
4
7
|
import React from 'react'
|
|
5
|
-
|
|
8
|
+
// @ts-expect-error no types
|
|
9
|
+
import * as ReactWindow from 'react-window'
|
|
10
|
+
import { expect } from 'vitest'
|
|
6
11
|
|
|
7
12
|
import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
8
13
|
import * as LiveStoreReact from './mod.js'
|
|
14
|
+
import { __resetUseRcResourceCache } from './useRcResource.js'
|
|
9
15
|
|
|
10
|
-
describe(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
|
|
17
|
+
'useQuery (strictMode=%s)',
|
|
18
|
+
({ strictMode }) => {
|
|
19
|
+
Vitest.afterEach(() => {
|
|
20
|
+
RG.__resetIds()
|
|
21
|
+
__resetUseRcResourceCache()
|
|
22
|
+
})
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
Vitest.scopedLive('simple', () =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.schema) })
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const { result } = ReactTesting.renderHook(
|
|
31
|
+
() => {
|
|
32
|
+
renderCount.inc()
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
return LiveStoreReact.useQuery(allTodos$)
|
|
35
|
+
},
|
|
36
|
+
{ wrapper },
|
|
37
|
+
)
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
expect(result.current.length).toBe(0)
|
|
40
|
+
expect(renderCount.val).toBe(1)
|
|
41
|
+
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
console.log('before mutation')
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
expect(result.current[0]!.text).toBe('buy milk')
|
|
35
|
-
expect(renderCount.val).toBe(2)
|
|
36
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
45
|
+
ReactTesting.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
Effect.gen(function* () {
|
|
40
|
-
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
47
|
+
console.log('after mutation')
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
})
|
|
49
|
+
expect(result.current.length).toBe(1)
|
|
50
|
+
expect(result.current[0]!.text).toBe('buy milk')
|
|
51
|
+
expect(renderCount.val).toBe(2)
|
|
52
|
+
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Vitest.scopedLive('same `useQuery` hook invoked with different queries', () =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
|
|
59
|
+
|
|
60
|
+
const todo1$ = queryDb(
|
|
61
|
+
{ query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.schema) },
|
|
62
|
+
{ label: 'libraryTracksView1' },
|
|
63
|
+
)
|
|
64
|
+
const todo2$ = queryDb(
|
|
65
|
+
{ query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.schema) },
|
|
66
|
+
{ label: 'libraryTracksView2' },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
store.mutate(
|
|
70
|
+
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
71
|
+
todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const { result, rerender } = ReactTesting.renderHook(
|
|
75
|
+
(todoId: string) => {
|
|
76
|
+
renderCount.inc()
|
|
77
|
+
|
|
78
|
+
const query$ = React.useMemo(() => (todoId === 't1' ? todo1$ : todo2$), [todoId])
|
|
79
|
+
|
|
80
|
+
return LiveStoreReact.useQuery(query$)[0]!.text
|
|
81
|
+
},
|
|
82
|
+
{ wrapper, initialProps: 't1' },
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
expect(result.current).toBe('buy milk')
|
|
86
|
+
expect(renderCount.val).toBe(1)
|
|
87
|
+
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('1: after first render')
|
|
88
|
+
|
|
89
|
+
ReactTesting.act(() => store.mutate(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
|
|
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
|
+
)
|
package/src/useQuery.ts
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
|
-
import type { GetResult,
|
|
1
|
+
import type { GetResult, LiveQuery, LiveQueryDef, LiveQueryDefAny, RcRef, Store } 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'
|
|
8
9
|
import { originalStackLimit } from './utils/stack-info.js'
|
|
9
10
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Returns the result of a query and subscribes to future updates.
|
|
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
|
+
* ```
|
|
20
22
|
*/
|
|
21
|
-
const
|
|
23
|
+
export const useQuery = <TQuery extends LiveQueryDefAny>(
|
|
24
|
+
queryDef: TQuery,
|
|
25
|
+
options?: { store?: Store },
|
|
26
|
+
): GetResult<TQuery> => useQueryRef(queryDef, options).valueRef.current
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
type GetQueryInfo<TQuery extends LiveQueryDefAny> =
|
|
29
|
+
TQuery extends LiveQueryDef<infer _1, infer TQueryInfo> ? TQueryInfo : never
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
|
-
*
|
|
27
32
|
*/
|
|
28
|
-
export const useQueryRef = <TQuery extends
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
export const useQueryRef = <TQuery extends LiveQueryDefAny>(
|
|
34
|
+
queryDef: TQuery,
|
|
35
|
+
options?: {
|
|
36
|
+
store?: Store
|
|
37
|
+
/** Parent otel context for the query */
|
|
38
|
+
otelContext?: otel.Context
|
|
39
|
+
/** The name of the span to use for the query */
|
|
40
|
+
otelSpanName?: string
|
|
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}`
|
|
36
49
|
|
|
37
50
|
const stackInfo = React.useMemo(() => {
|
|
38
51
|
Error.stackTraceLimit = 10
|
|
@@ -42,31 +55,42 @@ export const useQueryRef = <TQuery extends LiveQueryAny>(
|
|
|
42
55
|
return extractStackInfoFromStackTrace(stack)
|
|
43
56
|
}, [])
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
const { queryRcRef, span, otelContext } = useRcResource(
|
|
59
|
+
rcRefKey,
|
|
60
|
+
() => {
|
|
61
|
+
const queryDefLabel = queryDef.label
|
|
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
|
+
)
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
`LiveStore:useQuery:${query$.label}`,
|
|
52
|
-
{ attributes: { label: query$.label, stackInfo: JSON.stringify(stackInfo) } },
|
|
53
|
-
parentOtelContext ?? store.otel.queriesSpanContext,
|
|
54
|
-
)
|
|
69
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
55
70
|
|
|
56
|
-
|
|
71
|
+
const queryRcRef = queryDef.make(store.reactivityGraph.context!, otelContext)
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
return { queryRcRef, span, otelContext }
|
|
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>>
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
82
|
+
// console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
62
83
|
|
|
63
84
|
const initialResult = React.useMemo(() => {
|
|
64
85
|
try {
|
|
65
|
-
return query$.run(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
return query$.run({
|
|
87
|
+
otelContext,
|
|
88
|
+
debugRefreshReason: {
|
|
89
|
+
_tag: 'react',
|
|
90
|
+
api: 'useQuery',
|
|
91
|
+
label: `useQuery:initial-run:${query$.label}`,
|
|
92
|
+
stackInfo,
|
|
93
|
+
},
|
|
70
94
|
})
|
|
71
95
|
} catch (cause: any) {
|
|
72
96
|
throw new Error(
|
|
@@ -89,25 +113,20 @@ Stack trace:
|
|
|
89
113
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
90
114
|
const [valueRef, setValue] = useStateRefWithReactiveInput<GetResult<TQuery>>(initialResult)
|
|
91
115
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
spanAlreadyStartedCache.delete(query$)
|
|
95
|
-
span.end()
|
|
96
|
-
},
|
|
97
|
-
[query$, span],
|
|
98
|
-
)
|
|
116
|
+
// TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
|
|
117
|
+
// before calling the LS `onEffect` on it
|
|
99
118
|
|
|
100
119
|
// Subscribe to future updates for this query
|
|
101
120
|
React.useEffect(() => {
|
|
121
|
+
// TODO double check whether we still need `activeSubscriptions`
|
|
102
122
|
query$.activeSubscriptions.add(stackInfo)
|
|
103
123
|
|
|
104
124
|
// Dynamic queries only set their actual label after they've been run the first time,
|
|
105
125
|
// so we're also updating the span name here.
|
|
106
|
-
span.updateName(`LiveStore:useQuery:${query$.label}`)
|
|
126
|
+
span.updateName(options?.otelSpanName ?? `LiveStore:useQuery:${query$.label}`)
|
|
107
127
|
|
|
108
|
-
return store.subscribe(
|
|
109
|
-
|
|
110
|
-
(newValue) => {
|
|
128
|
+
return store.subscribe(query$, {
|
|
129
|
+
onUpdate: (newValue) => {
|
|
111
130
|
// NOTE: we return a reference to the result object within LiveStore;
|
|
112
131
|
// this implies that app code must not mutate the results, or else
|
|
113
132
|
// there may be weird reactivity bugs.
|
|
@@ -115,12 +134,23 @@ Stack trace:
|
|
|
115
134
|
setValue(newValue)
|
|
116
135
|
}
|
|
117
136
|
},
|
|
118
|
-
() => {
|
|
137
|
+
onUnsubsubscribe: () => {
|
|
119
138
|
query$.activeSubscriptions.delete(stackInfo)
|
|
120
139
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
label: query$.label,
|
|
141
|
+
otelContext,
|
|
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
|
+
)
|
|
124
154
|
|
|
125
|
-
return valueRef
|
|
155
|
+
return { valueRef, queryRcRef }
|
|
126
156
|
}
|