@livestore/react 0.0.0-snapshot-8d3edf87cb1e88c7b67c5f3ea9d0b307253c33df
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 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/LiveStoreContext.d.ts +7 -0
- package/dist/LiveStoreContext.d.ts.map +1 -0
- package/dist/LiveStoreContext.js +13 -0
- package/dist/LiveStoreContext.js.map +1 -0
- package/dist/LiveStoreProvider.d.ts +49 -0
- package/dist/LiveStoreProvider.d.ts.map +1 -0
- package/dist/LiveStoreProvider.js +168 -0
- package/dist/LiveStoreProvider.js.map +1 -0
- package/dist/LiveStoreProvider.test.d.ts +2 -0
- package/dist/LiveStoreProvider.test.d.ts.map +1 -0
- package/dist/LiveStoreProvider.test.js +62 -0
- package/dist/LiveStoreProvider.test.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +567 -0
- package/dist/__tests__/fixture.d.ts.map +1 -0
- package/dist/__tests__/fixture.js +61 -0
- package/dist/__tests__/fixture.js.map +1 -0
- package/dist/experimental/components/LiveList.d.ts +21 -0
- package/dist/experimental/components/LiveList.d.ts.map +1 -0
- package/dist/experimental/components/LiveList.js +31 -0
- package/dist/experimental/components/LiveList.js.map +1 -0
- package/dist/experimental/mod.d.ts +2 -0
- package/dist/experimental/mod.d.ts.map +1 -0
- package/dist/experimental/mod.js +2 -0
- package/dist/experimental/mod.js.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +8 -0
- package/dist/mod.js.map +1 -0
- package/dist/useAtom.d.ts +10 -0
- package/dist/useAtom.d.ts.map +1 -0
- package/dist/useAtom.js +37 -0
- package/dist/useAtom.js.map +1 -0
- package/dist/useQuery.d.ts +9 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useQuery.js +88 -0
- package/dist/useQuery.js.map +1 -0
- package/dist/useQuery.test.d.ts +2 -0
- package/dist/useQuery.test.d.ts.map +1 -0
- package/dist/useQuery.test.js +51 -0
- package/dist/useQuery.test.js.map +1 -0
- package/dist/useRow.d.ts +46 -0
- package/dist/useRow.d.ts.map +1 -0
- package/dist/useRow.js +96 -0
- package/dist/useRow.js.map +1 -0
- package/dist/useRow.test.d.ts +2 -0
- package/dist/useRow.test.d.ts.map +1 -0
- package/dist/useRow.test.js +212 -0
- package/dist/useRow.test.js.map +1 -0
- package/dist/useTemporaryQuery.d.ts +22 -0
- package/dist/useTemporaryQuery.d.ts.map +1 -0
- package/dist/useTemporaryQuery.js +75 -0
- package/dist/useTemporaryQuery.js.map +1 -0
- package/dist/useTemporaryQuery.test.d.ts +2 -0
- package/dist/useTemporaryQuery.test.d.ts.map +1 -0
- package/dist/useTemporaryQuery.test.js +59 -0
- package/dist/useTemporaryQuery.test.js.map +1 -0
- package/dist/utils/stack-info.d.ts +4 -0
- package/dist/utils/stack-info.d.ts.map +1 -0
- package/dist/utils/stack-info.js +11 -0
- package/dist/utils/stack-info.js.map +1 -0
- package/dist/utils/useStateRefWithReactiveInput.d.ts +13 -0
- package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -0
- package/dist/utils/useStateRefWithReactiveInput.js +38 -0
- package/dist/utils/useStateRefWithReactiveInput.js.map +1 -0
- package/package.json +54 -0
- package/src/LiveStoreContext.ts +19 -0
- package/src/LiveStoreProvider.test.tsx +109 -0
- package/src/LiveStoreProvider.tsx +295 -0
- package/src/__snapshots__/useRow.test.tsx.snap +359 -0
- package/src/__tests__/fixture.tsx +119 -0
- package/src/ambient.d.ts +2 -0
- package/src/experimental/components/LiveList.tsx +84 -0
- package/src/experimental/mod.ts +1 -0
- package/src/mod.ts +13 -0
- package/src/useAtom.ts +55 -0
- package/src/useQuery.test.tsx +82 -0
- package/src/useQuery.ts +122 -0
- package/src/useRow.test.tsx +346 -0
- package/src/useRow.ts +182 -0
- package/src/useTemporaryQuery.test.tsx +98 -0
- package/src/useTemporaryQuery.ts +131 -0
- package/src/utils/stack-info.ts +13 -0
- package/src/utils/useStateRefWithReactiveInput.ts +51 -0
- package/tsconfig.json +20 -0
- package/vitest.config.js +17 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { querySQL } from '@livestore/livestore'
|
|
2
|
+
import { Effect, Schema } from '@livestore/utils/effect'
|
|
3
|
+
import { renderHook } from '@testing-library/react'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
8
|
+
import * as LiveStoreReact from './mod.js'
|
|
9
|
+
|
|
10
|
+
describe('useQuery', () => {
|
|
11
|
+
it('simple', () =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
14
|
+
|
|
15
|
+
const renderCount = makeRenderCount()
|
|
16
|
+
|
|
17
|
+
const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
|
|
18
|
+
|
|
19
|
+
const { result } = renderHook(
|
|
20
|
+
() => {
|
|
21
|
+
renderCount.inc()
|
|
22
|
+
|
|
23
|
+
return LiveStoreReact.useQuery(allTodos$)
|
|
24
|
+
},
|
|
25
|
+
{ wrapper },
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(result.current.length).toBe(0)
|
|
29
|
+
expect(renderCount.val).toBe(1)
|
|
30
|
+
|
|
31
|
+
React.act(() => store.mutate(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
|
|
32
|
+
|
|
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))
|
|
37
|
+
|
|
38
|
+
it('same `useQuery` hook invoked with different queries', () =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
41
|
+
|
|
42
|
+
const renderCount = makeRenderCount()
|
|
43
|
+
|
|
44
|
+
const todo1$ = querySQL(`select * from todos where id = 't1'`, {
|
|
45
|
+
label: 'libraryTracksView1',
|
|
46
|
+
schema: Schema.Array(tables.todos.schema),
|
|
47
|
+
})
|
|
48
|
+
const todo2$ = querySQL(`select * from todos where id = 't2'`, {
|
|
49
|
+
label: 'libraryTracksView2',
|
|
50
|
+
schema: Schema.Array(tables.todos.schema),
|
|
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
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { GetResult, LiveQueryAny } from '@livestore/livestore'
|
|
2
|
+
import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
|
|
3
|
+
import { deepEqual, indent } from '@livestore/utils'
|
|
4
|
+
import * as otel from '@opentelemetry/api'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
import { useStore } from './LiveStoreContext.js'
|
|
8
|
+
import { originalStackLimit } from './utils/stack-info.js'
|
|
9
|
+
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
|
|
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
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
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}`)
|
|
36
|
+
|
|
37
|
+
const stackInfo = React.useMemo(() => {
|
|
38
|
+
Error.stackTraceLimit = 10
|
|
39
|
+
// eslint-disable-next-line unicorn/error-message
|
|
40
|
+
const stack = new Error().stack!
|
|
41
|
+
Error.stackTraceLimit = originalStackLimit
|
|
42
|
+
return extractStackInfoFromStackTrace(stack)
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
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
|
|
49
|
+
|
|
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
|
+
)
|
|
55
|
+
|
|
56
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
57
|
+
|
|
58
|
+
spanAlreadyStartedCache.set(query$, { span, otelContext })
|
|
59
|
+
|
|
60
|
+
return { span, otelContext }
|
|
61
|
+
}, [parentOtelContext, query$, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
62
|
+
|
|
63
|
+
const initialResult = React.useMemo(() => {
|
|
64
|
+
try {
|
|
65
|
+
return query$.run(otelContext, {
|
|
66
|
+
_tag: 'react',
|
|
67
|
+
api: 'useQuery',
|
|
68
|
+
label: query$.label,
|
|
69
|
+
stackInfo,
|
|
70
|
+
})
|
|
71
|
+
} catch (cause: any) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`\
|
|
74
|
+
[@livestore/react:useQuery] Error running query: ${cause.name}
|
|
75
|
+
|
|
76
|
+
Query: ${query$.label}
|
|
77
|
+
|
|
78
|
+
React trace:
|
|
79
|
+
|
|
80
|
+
${indent(stackInfoToString(stackInfo), 4)}
|
|
81
|
+
|
|
82
|
+
Stack trace:
|
|
83
|
+
`,
|
|
84
|
+
{ cause },
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}, [otelContext, query$, stackInfo])
|
|
88
|
+
|
|
89
|
+
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
90
|
+
const [valueRef, setValue] = useStateRefWithReactiveInput<GetResult<TQuery>>(initialResult)
|
|
91
|
+
|
|
92
|
+
React.useEffect(
|
|
93
|
+
() => () => {
|
|
94
|
+
spanAlreadyStartedCache.delete(query$)
|
|
95
|
+
span.end()
|
|
96
|
+
},
|
|
97
|
+
[query$, span],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Subscribe to future updates for this query
|
|
101
|
+
React.useEffect(() => {
|
|
102
|
+
query$.activeSubscriptions.add(stackInfo)
|
|
103
|
+
|
|
104
|
+
return store.subscribe(
|
|
105
|
+
query$,
|
|
106
|
+
(newValue) => {
|
|
107
|
+
// NOTE: we return a reference to the result object within LiveStore;
|
|
108
|
+
// this implies that app code must not mutate the results, or else
|
|
109
|
+
// there may be weird reactivity bugs.
|
|
110
|
+
if (deepEqual(newValue, valueRef.current) === false) {
|
|
111
|
+
setValue(newValue)
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
() => {
|
|
115
|
+
query$.activeSubscriptions.delete(stackInfo)
|
|
116
|
+
},
|
|
117
|
+
{ label: query$.label, otelContext },
|
|
118
|
+
)
|
|
119
|
+
}, [stackInfo, query$, setValue, store, valueRef, otelContext, span])
|
|
120
|
+
|
|
121
|
+
return valueRef
|
|
122
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import * as LiveStore from '@livestore/livestore'
|
|
2
|
+
import { getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
3
|
+
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
4
|
+
import * as otel from '@opentelemetry/api'
|
|
5
|
+
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
6
|
+
import { render, renderHook } from '@testing-library/react'
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
|
|
10
|
+
import { AppComponentSchema, AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
11
|
+
import * as LiveStoreReact from './mod.js'
|
|
12
|
+
|
|
13
|
+
// NOTE running tests concurrently doesn't work with the default global db graph
|
|
14
|
+
describe('useRow', () => {
|
|
15
|
+
it('should update the data based on component key', () =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
|
|
18
|
+
useGlobalReactivityGraph: false,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const renderCount = makeRenderCount()
|
|
22
|
+
|
|
23
|
+
const { result, rerender } = renderHook(
|
|
24
|
+
(userId: string) => {
|
|
25
|
+
renderCount.inc()
|
|
26
|
+
|
|
27
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
28
|
+
return { state, setState }
|
|
29
|
+
},
|
|
30
|
+
{ wrapper, initialProps: 'u1' },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expect(result.current.state.id).toBe('u1')
|
|
34
|
+
expect(result.current.state.username).toBe('')
|
|
35
|
+
expect(renderCount.val).toBe(1)
|
|
36
|
+
|
|
37
|
+
React.act(() =>
|
|
38
|
+
store.mutate(
|
|
39
|
+
LiveStore.rawSqlMutation({
|
|
40
|
+
sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
rerender('u2')
|
|
46
|
+
|
|
47
|
+
expect(result.current.state.id).toBe('u2')
|
|
48
|
+
expect(result.current.state.username).toBe('username_u2')
|
|
49
|
+
expect(renderCount.val).toBe(2)
|
|
50
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
51
|
+
|
|
52
|
+
// TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
|
|
53
|
+
|
|
54
|
+
it('should update the data reactively - via setState', () =>
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
|
|
57
|
+
useGlobalReactivityGraph: false,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const renderCount = makeRenderCount()
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(
|
|
63
|
+
(userId: string) => {
|
|
64
|
+
renderCount.inc()
|
|
65
|
+
|
|
66
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
67
|
+
return { state, setState }
|
|
68
|
+
},
|
|
69
|
+
{ wrapper, initialProps: 'u1' },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(result.current.state.id).toBe('u1')
|
|
73
|
+
expect(result.current.state.username).toBe('')
|
|
74
|
+
expect(renderCount.val).toBe(1)
|
|
75
|
+
|
|
76
|
+
React.act(() => result.current.setState.username('username_u1_hello'))
|
|
77
|
+
|
|
78
|
+
expect(result.current.state.id).toBe('u1')
|
|
79
|
+
expect(result.current.state.username).toBe('username_u1_hello')
|
|
80
|
+
expect(renderCount.val).toBe(2)
|
|
81
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
82
|
+
|
|
83
|
+
it('should update the data reactively - via raw store mutation', () =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
|
|
86
|
+
useGlobalReactivityGraph: false,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const renderCount = makeRenderCount()
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(
|
|
92
|
+
(userId: string) => {
|
|
93
|
+
renderCount.inc()
|
|
94
|
+
|
|
95
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
96
|
+
return { state, setState }
|
|
97
|
+
},
|
|
98
|
+
{ wrapper, initialProps: 'u1' },
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(result.current.state.id).toBe('u1')
|
|
102
|
+
expect(result.current.state.username).toBe('')
|
|
103
|
+
expect(renderCount.val).toBe(1)
|
|
104
|
+
|
|
105
|
+
React.act(() =>
|
|
106
|
+
store.mutate(
|
|
107
|
+
LiveStore.rawSqlMutation({
|
|
108
|
+
sql: LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`,
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
expect(result.current.state.id).toBe('u1')
|
|
114
|
+
expect(result.current.state.username).toBe('username_u1_hello')
|
|
115
|
+
expect(renderCount.val).toBe(2)
|
|
116
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
117
|
+
|
|
118
|
+
it('should work for a larger app', () =>
|
|
119
|
+
Effect.gen(function* () {
|
|
120
|
+
const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
|
|
121
|
+
useGlobalReactivityGraph: false,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const allTodos$ = LiveStore.querySQL(`select * from todos`, {
|
|
125
|
+
label: 'allTodos',
|
|
126
|
+
schema: Schema.Array(tables.todos.schema),
|
|
127
|
+
reactivityGraph,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const appRouterRenderCount = makeRenderCount()
|
|
131
|
+
let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
|
|
132
|
+
const AppRouter: React.FC = () => {
|
|
133
|
+
appRouterRenderCount.inc()
|
|
134
|
+
|
|
135
|
+
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
|
|
136
|
+
|
|
137
|
+
globalSetState = setState
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
<TasksList setTaskId={setState.currentTaskId} />
|
|
142
|
+
<div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
|
|
143
|
+
{state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
|
|
149
|
+
const allTodos = LiveStoreReact.useQuery(allTodos$)
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div>
|
|
153
|
+
{allTodos.map((_) => (
|
|
154
|
+
<div key={_.id} onClick={() => setTaskId(_.id)}>
|
|
155
|
+
{_.id}
|
|
156
|
+
</div>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
|
|
163
|
+
const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
|
|
164
|
+
return <div role="content">{JSON.stringify(todo)}</div>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const renderResult = render(<AppRouter />, { wrapper })
|
|
168
|
+
|
|
169
|
+
expect(appRouterRenderCount.val).toBe(1)
|
|
170
|
+
|
|
171
|
+
React.act(() =>
|
|
172
|
+
store.mutate(
|
|
173
|
+
LiveStore.rawSqlMutation({
|
|
174
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(appRouterRenderCount.val).toBe(1)
|
|
180
|
+
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
|
|
181
|
+
|
|
182
|
+
React.act(() => globalSetState!.currentTaskId('t1'))
|
|
183
|
+
|
|
184
|
+
expect(appRouterRenderCount.val).toBe(2)
|
|
185
|
+
expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
|
|
186
|
+
`"{"id":"t1","text":"buy milk","completed":false}"`,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
|
|
190
|
+
|
|
191
|
+
React.act(() =>
|
|
192
|
+
store.mutate(
|
|
193
|
+
LiveStore.rawSqlMutation({
|
|
194
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
|
|
195
|
+
}),
|
|
196
|
+
AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
|
|
197
|
+
LiveStore.rawSqlMutation({
|
|
198
|
+
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
expect(appRouterRenderCount.val).toBe(3)
|
|
204
|
+
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
|
|
205
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
206
|
+
|
|
207
|
+
it('should work for a useRow query chained with a useTemporary query', () =>
|
|
208
|
+
Effect.gen(function* () {
|
|
209
|
+
const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
|
|
210
|
+
useGlobalReactivityGraph: false,
|
|
211
|
+
})
|
|
212
|
+
const renderCount = makeRenderCount()
|
|
213
|
+
|
|
214
|
+
store.mutate(
|
|
215
|
+
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
216
|
+
todos.insert({ id: 't2', text: 'buy bread', completed: false }),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const { result, unmount, rerender } = renderHook(
|
|
220
|
+
(userId: string) => {
|
|
221
|
+
renderCount.inc()
|
|
222
|
+
|
|
223
|
+
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
224
|
+
const todos = LiveStoreReact.useTemporaryQuery(
|
|
225
|
+
() =>
|
|
226
|
+
LiveStore.querySQL(
|
|
227
|
+
(get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
|
|
228
|
+
{
|
|
229
|
+
schema: Schema.Array(tables.todos.schema),
|
|
230
|
+
reactivityGraph,
|
|
231
|
+
label: 'todosFiltered',
|
|
232
|
+
},
|
|
233
|
+
),
|
|
234
|
+
userId,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return { todos }
|
|
238
|
+
},
|
|
239
|
+
{ wrapper, initialProps: 'u1' },
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
React.act(() =>
|
|
243
|
+
store.mutate(
|
|
244
|
+
LiveStore.rawSqlMutation({
|
|
245
|
+
sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
|
|
246
|
+
}),
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect(result.current.todos.length).toBe(2)
|
|
251
|
+
// expect(result.current.state.username).toBe('')
|
|
252
|
+
expect(renderCount.val).toBe(1)
|
|
253
|
+
|
|
254
|
+
rerender('u2')
|
|
255
|
+
|
|
256
|
+
expect(result.current.todos.length).toBe(1)
|
|
257
|
+
expect(renderCount.val).toBe(2)
|
|
258
|
+
|
|
259
|
+
unmount()
|
|
260
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
261
|
+
|
|
262
|
+
let cachedProvider: BasicTracerProvider | undefined
|
|
263
|
+
|
|
264
|
+
describe('otel', () => {
|
|
265
|
+
const exporter = new InMemorySpanExporter()
|
|
266
|
+
|
|
267
|
+
const provider = cachedProvider ?? new BasicTracerProvider()
|
|
268
|
+
cachedProvider = provider
|
|
269
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
|
|
270
|
+
provider.register()
|
|
271
|
+
|
|
272
|
+
const otelTracer = otel.trace.getTracer('test')
|
|
273
|
+
|
|
274
|
+
const span = otelTracer.startSpan('test')
|
|
275
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
276
|
+
|
|
277
|
+
it('should update the data based on component key', async () => {
|
|
278
|
+
const { strictMode } = await Effect.gen(function* () {
|
|
279
|
+
const { wrapper, store, reactivityGraph, makeRenderCount, strictMode } = yield* makeTodoMvcReact({
|
|
280
|
+
useGlobalReactivityGraph: false,
|
|
281
|
+
otelContext,
|
|
282
|
+
otelTracer,
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const renderCount = makeRenderCount()
|
|
286
|
+
|
|
287
|
+
const { result, rerender, unmount } = renderHook(
|
|
288
|
+
(userId: string) => {
|
|
289
|
+
renderCount.inc()
|
|
290
|
+
|
|
291
|
+
const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
|
|
292
|
+
return { state, setState }
|
|
293
|
+
},
|
|
294
|
+
{ wrapper, initialProps: 'u1' },
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
expect(result.current.state.id).toBe('u1')
|
|
298
|
+
expect(result.current.state.username).toBe('')
|
|
299
|
+
expect(renderCount.val).toBe(1)
|
|
300
|
+
|
|
301
|
+
React.act(() =>
|
|
302
|
+
store.mutate(
|
|
303
|
+
LiveStore.rawSqlMutation({
|
|
304
|
+
sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
rerender('u2')
|
|
310
|
+
|
|
311
|
+
expect(result.current.state.id).toBe('u2')
|
|
312
|
+
expect(result.current.state.username).toBe('username_u2')
|
|
313
|
+
expect(renderCount.val).toBe(2)
|
|
314
|
+
|
|
315
|
+
unmount()
|
|
316
|
+
span.end()
|
|
317
|
+
|
|
318
|
+
return { strictMode }
|
|
319
|
+
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
320
|
+
|
|
321
|
+
const mapAttributes = (attributes: otel.Attributes) => {
|
|
322
|
+
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
323
|
+
if (key === 'stackInfo') {
|
|
324
|
+
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
325
|
+
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
326
|
+
stackInfo.frames.forEach((_) => {
|
|
327
|
+
if (_.name.includes('renderHook.wrapper')) {
|
|
328
|
+
_.name = 'renderHook.wrapper'
|
|
329
|
+
}
|
|
330
|
+
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
|
331
|
+
})
|
|
332
|
+
return JSON.stringify(stackInfo)
|
|
333
|
+
}
|
|
334
|
+
return val
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// TODO improve testing setup so "obsolete" warning is avoided
|
|
339
|
+
if (strictMode) {
|
|
340
|
+
expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=true')
|
|
341
|
+
} else {
|
|
342
|
+
expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=false')
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|