@livestore/react 0.3.0-dev.10 → 0.3.0-dev.11
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.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 +16 -19
- 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 +1 -17
- package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
- package/src/__snapshots__/useRow.test.tsx.snap +335 -140
- 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 +32 -35
- package/src/useScopedQuery.test.tsx +0 -96
- package/src/useScopedQuery.ts +0 -143
package/src/useRow.test.tsx
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import * as LiveStore from '@livestore/livestore'
|
|
2
2
|
import { getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
3
3
|
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
4
|
+
import { Vitest } from '@livestore/utils/node-vitest'
|
|
4
5
|
import * as otel from '@opentelemetry/api'
|
|
5
6
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
6
|
-
import
|
|
7
|
+
import * as ReactTesting from '@testing-library/react'
|
|
7
8
|
import React from 'react'
|
|
8
|
-
import {
|
|
9
|
+
import { beforeEach, expect, it } from 'vitest'
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
11
12
|
import * as LiveStoreReact from './mod.js'
|
|
13
|
+
import { __resetUseRcResourceCache } from './useRcResource.js'
|
|
12
14
|
|
|
13
15
|
// const strictMode = process.env.REACT_STRICT_MODE !== undefined
|
|
14
16
|
|
|
15
17
|
// NOTE running tests concurrently doesn't work with the default global db graph
|
|
16
|
-
describe('useRow', () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
useGlobalReactivityGraph: false,
|
|
21
|
-
})
|
|
18
|
+
Vitest.describe('useRow', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
__resetUseRcResourceCache()
|
|
21
|
+
})
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Vitest.scopedLive('should update the data based on component key', () =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
24
26
|
|
|
25
|
-
const { result, rerender } = renderHook(
|
|
27
|
+
const { result, rerender } = ReactTesting.renderHook(
|
|
26
28
|
(userId: string) => {
|
|
27
29
|
renderCount.inc()
|
|
28
30
|
|
|
29
|
-
const [state, setState] = LiveStoreReact.useRow(
|
|
31
|
+
const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
|
|
30
32
|
return { state, setState }
|
|
31
33
|
},
|
|
32
34
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -35,37 +37,29 @@ describe('useRow', () => {
|
|
|
35
37
|
expect(result.current.state.id).toBe('u1')
|
|
36
38
|
expect(result.current.state.username).toBe('')
|
|
37
39
|
expect(renderCount.val).toBe(1)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
store.mutate(
|
|
41
|
-
LiveStore.rawSqlMutation({
|
|
42
|
-
sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
|
|
43
|
-
}),
|
|
44
|
-
),
|
|
45
|
-
)
|
|
40
|
+
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
41
|
+
store.mutate(tables.userInfo.insert({ id: 'u2', username: 'username_u2' }))
|
|
46
42
|
|
|
47
43
|
rerender('u2')
|
|
48
44
|
|
|
45
|
+
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
49
46
|
expect(result.current.state.id).toBe('u2')
|
|
50
47
|
expect(result.current.state.username).toBe('username_u2')
|
|
51
48
|
expect(renderCount.val).toBe(2)
|
|
52
|
-
})
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
53
51
|
|
|
54
52
|
// TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
Vitest.scopedLive('should update the data reactively - via setState', () =>
|
|
57
55
|
Effect.gen(function* () {
|
|
58
|
-
const { wrapper,
|
|
59
|
-
useGlobalReactivityGraph: false,
|
|
60
|
-
})
|
|
56
|
+
const { wrapper, renderCount } = yield* makeTodoMvcReact({})
|
|
61
57
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const { result } = renderHook(
|
|
58
|
+
const { result } = ReactTesting.renderHook(
|
|
65
59
|
(userId: string) => {
|
|
66
60
|
renderCount.inc()
|
|
67
61
|
|
|
68
|
-
const [state, setState] = LiveStoreReact.useRow(
|
|
62
|
+
const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
|
|
69
63
|
return { state, setState }
|
|
70
64
|
},
|
|
71
65
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -75,26 +69,23 @@ describe('useRow', () => {
|
|
|
75
69
|
expect(result.current.state.username).toBe('')
|
|
76
70
|
expect(renderCount.val).toBe(1)
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
ReactTesting.act(() => result.current.setState.username('username_u1_hello'))
|
|
79
73
|
|
|
80
74
|
expect(result.current.state.id).toBe('u1')
|
|
81
75
|
expect(result.current.state.username).toBe('username_u1_hello')
|
|
82
76
|
expect(renderCount.val).toBe(2)
|
|
83
|
-
})
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
Vitest.scopedLive('should update the data reactively - via raw store mutation', () =>
|
|
86
81
|
Effect.gen(function* () {
|
|
87
|
-
const { wrapper, store,
|
|
88
|
-
useGlobalReactivityGraph: false,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const renderCount = makeRenderCount()
|
|
82
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
92
83
|
|
|
93
|
-
const { result } = renderHook(
|
|
84
|
+
const { result } = ReactTesting.renderHook(
|
|
94
85
|
(userId: string) => {
|
|
95
86
|
renderCount.inc()
|
|
96
87
|
|
|
97
|
-
const [state, setState] = LiveStoreReact.useRow(
|
|
88
|
+
const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
|
|
98
89
|
return { state, setState }
|
|
99
90
|
},
|
|
100
91
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -104,36 +95,30 @@ describe('useRow', () => {
|
|
|
104
95
|
expect(result.current.state.username).toBe('')
|
|
105
96
|
expect(renderCount.val).toBe(1)
|
|
106
97
|
|
|
107
|
-
|
|
108
|
-
store.mutate(
|
|
109
|
-
LiveStore.rawSqlMutation({
|
|
110
|
-
sql: LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`,
|
|
111
|
-
}),
|
|
112
|
-
),
|
|
98
|
+
ReactTesting.act(() =>
|
|
99
|
+
store.mutate(tables.userInfo.update({ where: { id: 'u1' }, values: { username: 'username_u1_hello' } })),
|
|
113
100
|
)
|
|
114
101
|
|
|
115
102
|
expect(result.current.state.id).toBe('u1')
|
|
116
103
|
expect(result.current.state.username).toBe('username_u1_hello')
|
|
117
104
|
expect(renderCount.val).toBe(2)
|
|
118
|
-
})
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
119
107
|
|
|
120
|
-
|
|
108
|
+
Vitest.scopedLive('should work for a larger app', () =>
|
|
121
109
|
Effect.gen(function* () {
|
|
122
|
-
const { wrapper, store,
|
|
123
|
-
useGlobalReactivityGraph: false,
|
|
124
|
-
})
|
|
110
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
125
111
|
|
|
126
112
|
const allTodos$ = LiveStore.queryDb(
|
|
127
113
|
{ query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
|
|
128
|
-
{ label: 'allTodos'
|
|
114
|
+
{ label: 'allTodos' },
|
|
129
115
|
)
|
|
130
116
|
|
|
131
|
-
const appRouterRenderCount = makeRenderCount()
|
|
132
117
|
let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
|
|
133
118
|
const AppRouter: React.FC = () => {
|
|
134
|
-
|
|
119
|
+
renderCount.inc()
|
|
135
120
|
|
|
136
|
-
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema
|
|
121
|
+
const [state, setState] = LiveStoreReact.useRow(AppRouterSchema)
|
|
137
122
|
|
|
138
123
|
globalSetState = setState
|
|
139
124
|
|
|
@@ -161,15 +146,15 @@ describe('useRow', () => {
|
|
|
161
146
|
}
|
|
162
147
|
|
|
163
148
|
const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
|
|
164
|
-
const [todo] = LiveStoreReact.useRow(todos, id
|
|
149
|
+
const [todo] = LiveStoreReact.useRow(todos, id)
|
|
165
150
|
return <div role="content">{JSON.stringify(todo)}</div>
|
|
166
151
|
}
|
|
167
152
|
|
|
168
|
-
const renderResult = render(<AppRouter />, { wrapper })
|
|
153
|
+
const renderResult = ReactTesting.render(<AppRouter />, { wrapper })
|
|
169
154
|
|
|
170
|
-
expect(
|
|
155
|
+
expect(renderCount.val).toBe(1)
|
|
171
156
|
|
|
172
|
-
|
|
157
|
+
ReactTesting.act(() =>
|
|
173
158
|
store.mutate(
|
|
174
159
|
LiveStore.rawSqlMutation({
|
|
175
160
|
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
|
|
@@ -177,19 +162,19 @@ describe('useRow', () => {
|
|
|
177
162
|
),
|
|
178
163
|
)
|
|
179
164
|
|
|
180
|
-
expect(
|
|
165
|
+
expect(renderCount.val).toBe(1)
|
|
181
166
|
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
|
|
182
167
|
|
|
183
|
-
|
|
168
|
+
ReactTesting.act(() => globalSetState!.currentTaskId('t1'))
|
|
184
169
|
|
|
185
|
-
expect(
|
|
170
|
+
expect(renderCount.val).toBe(2)
|
|
186
171
|
expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
|
|
187
172
|
`"{"id":"t1","text":"buy milk","completed":false}"`,
|
|
188
173
|
)
|
|
189
174
|
|
|
190
175
|
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
|
|
191
176
|
|
|
192
|
-
|
|
177
|
+
ReactTesting.act(() =>
|
|
193
178
|
store.mutate(
|
|
194
179
|
LiveStore.rawSqlMutation({
|
|
195
180
|
sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
|
|
@@ -201,37 +186,33 @@ describe('useRow', () => {
|
|
|
201
186
|
),
|
|
202
187
|
)
|
|
203
188
|
|
|
204
|
-
expect(
|
|
189
|
+
expect(renderCount.val).toBe(3)
|
|
205
190
|
expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
|
|
206
|
-
})
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
207
193
|
|
|
208
|
-
|
|
194
|
+
Vitest.scopedLive('should work for a useRow query chained with a useTemporary query', () =>
|
|
209
195
|
Effect.gen(function* () {
|
|
210
|
-
const { store, wrapper,
|
|
211
|
-
useGlobalReactivityGraph: false,
|
|
212
|
-
})
|
|
213
|
-
const renderCount = makeRenderCount()
|
|
196
|
+
const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
|
|
214
197
|
|
|
215
198
|
store.mutate(
|
|
216
199
|
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
217
200
|
todos.insert({ id: 't2', text: 'buy bread', completed: false }),
|
|
218
201
|
)
|
|
219
202
|
|
|
220
|
-
const { result, unmount, rerender } = renderHook(
|
|
203
|
+
const { result, unmount, rerender } = ReactTesting.renderHook(
|
|
221
204
|
(userId: string) => {
|
|
222
205
|
renderCount.inc()
|
|
223
206
|
|
|
224
|
-
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(
|
|
225
|
-
const todos = LiveStoreReact.
|
|
226
|
-
(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
),
|
|
234
|
-
userId,
|
|
207
|
+
const [_row, _setRow, rowState$] = LiveStoreReact.useRow(tables.userInfo, userId)
|
|
208
|
+
const todos = LiveStoreReact.useQuery(
|
|
209
|
+
LiveStore.queryDb(
|
|
210
|
+
(get) => tables.todos.query.where('text', 'LIKE', `%${get(rowState$).text}%`),
|
|
211
|
+
// TODO find a way where explicit `userId` is not needed here
|
|
212
|
+
// possibly by automatically understanding the `get(rowState$)` dependency
|
|
213
|
+
{ label: 'todosFiltered', deps: userId },
|
|
214
|
+
),
|
|
215
|
+
// TODO introduce a `deps` array which is only needed when a query is parametric
|
|
235
216
|
)
|
|
236
217
|
|
|
237
218
|
return { todos }
|
|
@@ -239,16 +220,9 @@ describe('useRow', () => {
|
|
|
239
220
|
{ wrapper, initialProps: 'u1' },
|
|
240
221
|
)
|
|
241
222
|
|
|
242
|
-
|
|
243
|
-
store.mutate(
|
|
244
|
-
LiveStore.rawSqlMutation({
|
|
245
|
-
sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
|
|
246
|
-
}),
|
|
247
|
-
),
|
|
248
|
-
)
|
|
223
|
+
ReactTesting.act(() => store.mutate(tables.userInfo.insert({ id: 'u2', username: 'username_u2', text: 'milk' })))
|
|
249
224
|
|
|
250
225
|
expect(result.current.todos.length).toBe(2)
|
|
251
|
-
// expect(result.current.state.username).toBe('')
|
|
252
226
|
expect(renderCount.val).toBe(1)
|
|
253
227
|
|
|
254
228
|
rerender('u2')
|
|
@@ -257,9 +231,10 @@ describe('useRow', () => {
|
|
|
257
231
|
expect(renderCount.val).toBe(2)
|
|
258
232
|
|
|
259
233
|
unmount()
|
|
260
|
-
})
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
261
236
|
|
|
262
|
-
describe('otel', () => {
|
|
237
|
+
Vitest.describe('otel', () => {
|
|
263
238
|
const provider = new BasicTracerProvider({})
|
|
264
239
|
provider.register()
|
|
265
240
|
|
|
@@ -277,20 +252,17 @@ describe('useRow', () => {
|
|
|
277
252
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
278
253
|
|
|
279
254
|
await Effect.gen(function* () {
|
|
280
|
-
const { wrapper, store,
|
|
281
|
-
useGlobalReactivityGraph: false,
|
|
255
|
+
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
|
|
282
256
|
otelContext,
|
|
283
257
|
otelTracer,
|
|
284
258
|
strictMode,
|
|
285
259
|
})
|
|
286
260
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const { result, rerender, unmount } = renderHook(
|
|
261
|
+
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
290
262
|
(userId: string) => {
|
|
291
263
|
renderCount.inc()
|
|
292
264
|
|
|
293
|
-
const [state, setState] = LiveStoreReact.useRow(
|
|
265
|
+
const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
|
|
294
266
|
return { state, setState }
|
|
295
267
|
},
|
|
296
268
|
{ wrapper, initialProps: 'u1' },
|
|
@@ -300,13 +272,9 @@ describe('useRow', () => {
|
|
|
300
272
|
expect(result.current.state.username).toBe('')
|
|
301
273
|
expect(renderCount.val).toBe(1)
|
|
302
274
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
|
|
307
|
-
}),
|
|
308
|
-
),
|
|
309
|
-
)
|
|
275
|
+
// For u2 we'll make sure that the row already exists,
|
|
276
|
+
// so the lazy `insert` will be skipped
|
|
277
|
+
ReactTesting.act(() => store.mutate(tables.userInfo.insert({ id: 'u2', username: 'username_u2' })))
|
|
310
278
|
|
|
311
279
|
rerender('u2')
|
|
312
280
|
|
|
@@ -316,13 +284,11 @@ describe('useRow', () => {
|
|
|
316
284
|
|
|
317
285
|
unmount()
|
|
318
286
|
span.end()
|
|
319
|
-
|
|
320
|
-
return { strictMode }
|
|
321
287
|
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
|
|
322
288
|
|
|
323
289
|
const mapAttributes = (attributes: otel.Attributes) => {
|
|
324
290
|
return ReadonlyRecord.map(attributes, (val, key) => {
|
|
325
|
-
if (key === '
|
|
291
|
+
if (key === 'firstStackInfo') {
|
|
326
292
|
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
327
293
|
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
328
294
|
stackInfo.frames.forEach((_) => {
|
package/src/useRow.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { QueryInfo, RowQuery } from '@livestore/common'
|
|
|
2
2
|
import { SessionIdSymbol } from '@livestore/common'
|
|
3
3
|
import { DbSchema } from '@livestore/common/schema'
|
|
4
4
|
import type { SqliteDsl } from '@livestore/db-schema'
|
|
5
|
-
import type { LiveQuery,
|
|
5
|
+
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
6
6
|
import { queryDb } from '@livestore/livestore'
|
|
7
7
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
8
8
|
import { ReadonlyRecord } from '@livestore/utils/effect'
|
|
@@ -10,7 +10,6 @@ import React from 'react'
|
|
|
10
10
|
|
|
11
11
|
import { useStore } from './LiveStoreContext.js'
|
|
12
12
|
import { useQueryRef } from './useQuery.js'
|
|
13
|
-
import { useMakeScopedQuery } from './useScopedQuery.js'
|
|
14
13
|
|
|
15
14
|
export type UseRowResult<TTableDef extends DbSchema.TableDefBase> = [
|
|
16
15
|
row: RowQuery.Result<TTableDef>,
|
|
@@ -18,10 +17,6 @@ export type UseRowResult<TTableDef extends DbSchema.TableDefBase> = [
|
|
|
18
17
|
query$: LiveQuery<RowQuery.Result<TTableDef>, QueryInfo>,
|
|
19
18
|
]
|
|
20
19
|
|
|
21
|
-
export type UseRowOptionsBase = {
|
|
22
|
-
reactivityGraph?: ReactivityGraph
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
/**
|
|
26
21
|
* Similar to `React.useState` but returns a tuple of `[row, setRow, query$]` for a given table where ...
|
|
27
22
|
*
|
|
@@ -32,6 +27,7 @@ export type UseRowOptionsBase = {
|
|
|
32
27
|
* If the table is a singleton table, `useRow` can be called without an `id` argument. Otherwise, the `id` argument is required.
|
|
33
28
|
*/
|
|
34
29
|
export const useRow: {
|
|
30
|
+
// isSingleton: true
|
|
35
31
|
<
|
|
36
32
|
TTableDef extends DbSchema.TableDef<
|
|
37
33
|
DbSchema.DefaultSqliteTableDef,
|
|
@@ -39,8 +35,10 @@ export const useRow: {
|
|
|
39
35
|
>,
|
|
40
36
|
>(
|
|
41
37
|
table: TTableDef,
|
|
42
|
-
options?:
|
|
38
|
+
options?: { store?: Store },
|
|
43
39
|
): UseRowResult<TTableDef>
|
|
40
|
+
|
|
41
|
+
// isSingleton: false with requiredInsertColumnNames: 'id'
|
|
44
42
|
<
|
|
45
43
|
TTableDef extends DbSchema.TableDef<
|
|
46
44
|
DbSchema.DefaultSqliteTableDef,
|
|
@@ -54,8 +52,10 @@ export const useRow: {
|
|
|
54
52
|
table: TTableDef,
|
|
55
53
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
56
54
|
id: string | SessionIdSymbol,
|
|
57
|
-
options?:
|
|
55
|
+
options?: Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store },
|
|
58
56
|
): UseRowResult<TTableDef>
|
|
57
|
+
|
|
58
|
+
// isSingleton: false
|
|
59
59
|
<
|
|
60
60
|
TTableDef extends DbSchema.TableDef<
|
|
61
61
|
DbSchema.DefaultSqliteTableDef,
|
|
@@ -65,7 +65,7 @@ export const useRow: {
|
|
|
65
65
|
table: TTableDef,
|
|
66
66
|
// TODO adjust so it works with arbitrary primary keys or unique constraints
|
|
67
67
|
id: string | SessionIdSymbol,
|
|
68
|
-
options:
|
|
68
|
+
options: RowQuery.RequiredColumnsOptions<TTableDef> & { store?: Store },
|
|
69
69
|
): UseRowResult<TTableDef>
|
|
70
70
|
} = <
|
|
71
71
|
TTableDef extends DbSchema.TableDef<
|
|
@@ -74,14 +74,14 @@ export const useRow: {
|
|
|
74
74
|
>,
|
|
75
75
|
>(
|
|
76
76
|
table: TTableDef,
|
|
77
|
-
idOrOptions?: string | SessionIdSymbol |
|
|
78
|
-
options_?:
|
|
77
|
+
idOrOptions?: string | SessionIdSymbol | { store?: Store },
|
|
78
|
+
options_?: Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store },
|
|
79
79
|
): UseRowResult<TTableDef> => {
|
|
80
80
|
const sqliteTableDef = table.sqliteDef
|
|
81
81
|
const id = typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? idOrOptions : undefined
|
|
82
|
-
const options: (
|
|
82
|
+
const options: (Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store }) | undefined =
|
|
83
83
|
typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? options_ : idOrOptions
|
|
84
|
-
const { insertValues
|
|
84
|
+
const { insertValues } = options ?? {}
|
|
85
85
|
|
|
86
86
|
type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
|
|
87
87
|
|
|
@@ -91,7 +91,7 @@ export const useRow: {
|
|
|
91
91
|
shouldNeverHappen(`useRow called on table "${tableName}" which does not have 'deriveMutations: true' set`)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const { store } = useStore()
|
|
94
|
+
const { store } = useStore({ store: options?.store })
|
|
95
95
|
|
|
96
96
|
if (
|
|
97
97
|
store.schema.tables.has(table.sqliteDef.name) === false &&
|
|
@@ -105,28 +105,25 @@ export const useRow: {
|
|
|
105
105
|
const idStr = id === SessionIdSymbol ? 'session' : id
|
|
106
106
|
const rowQuery = table.query.row as any
|
|
107
107
|
|
|
108
|
-
type
|
|
109
|
-
const
|
|
110
|
-
(
|
|
108
|
+
type QueryDef = LiveQueryDef<RowQuery.Result<TTableDef>, QueryInfo.Row>
|
|
109
|
+
const queryDef: QueryDef = React.useMemo(
|
|
110
|
+
() =>
|
|
111
111
|
DbSchema.tableIsSingleton(table)
|
|
112
|
-
?
|
|
113
|
-
:
|
|
114
|
-
[idStr
|
|
115
|
-
{
|
|
116
|
-
otel: {
|
|
117
|
-
spanName: `LiveStore:useRow:${tableName}${idStr === undefined ? '' : `:${idStr}`}`,
|
|
118
|
-
attributes: { id: idStr },
|
|
119
|
-
},
|
|
120
|
-
},
|
|
112
|
+
? queryDb(rowQuery(), {})
|
|
113
|
+
: queryDb(rowQuery(id!, { insertValues: insertValues! }), { deps: idStr! }),
|
|
114
|
+
[id, insertValues, rowQuery, table, idStr],
|
|
121
115
|
)
|
|
122
116
|
|
|
123
|
-
const
|
|
117
|
+
const queryRef = useQueryRef(queryDef, {
|
|
118
|
+
otelSpanName: `LiveStore:useRow:${tableName}${idStr === undefined ? '' : `:${idStr}`}`,
|
|
119
|
+
store: options?.store,
|
|
120
|
+
})
|
|
124
121
|
|
|
125
122
|
const setState = React.useMemo<StateSetters<TTableDef>>(() => {
|
|
126
123
|
if (table.options.isSingleColumn) {
|
|
127
124
|
return (newValueOrFn: RowQuery.Result<TTableDef>) => {
|
|
128
|
-
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(
|
|
129
|
-
if (
|
|
125
|
+
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
|
|
126
|
+
if (queryRef.valueRef.current === newValue) return
|
|
130
127
|
|
|
131
128
|
// NOTE we need to account for the short-hand syntax for single-column+singleton tables
|
|
132
129
|
if (table.options.isSingleton) {
|
|
@@ -141,11 +138,11 @@ export const useRow: {
|
|
|
141
138
|
ReadonlyRecord.map(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
|
|
142
139
|
const newValue =
|
|
143
140
|
// @ts-expect-error TODO fix typing
|
|
144
|
-
typeof newValueOrFn === 'function' ? newValueOrFn(
|
|
141
|
+
typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current[columnName]) : newValueOrFn
|
|
145
142
|
|
|
146
143
|
// Don't update the state if it's the same as the value already seen in the component
|
|
147
144
|
// @ts-expect-error TODO fix typing
|
|
148
|
-
if (
|
|
145
|
+
if (queryRef.valueRef.current[columnName] === newValue) return
|
|
149
146
|
|
|
150
147
|
store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: { [columnName]: newValue } }))
|
|
151
148
|
// store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
|
|
@@ -154,13 +151,13 @@ export const useRow: {
|
|
|
154
151
|
setState.setMany = (columnValuesOrFn: Partial<TComponentState>) => {
|
|
155
152
|
const columnValues =
|
|
156
153
|
// @ts-expect-error TODO fix typing
|
|
157
|
-
typeof columnValuesOrFn === 'function' ? columnValuesOrFn(
|
|
154
|
+
typeof columnValuesOrFn === 'function' ? columnValuesOrFn(queryRef.valueRef.current) : columnValuesOrFn
|
|
158
155
|
|
|
159
156
|
// TODO use hashing instead
|
|
160
157
|
// Don't update the state if it's the same as the value already seen in the component
|
|
161
158
|
if (
|
|
162
159
|
// @ts-expect-error TODO fix typing
|
|
163
|
-
Object.entries(columnValues).every(([columnName, value]) =>
|
|
160
|
+
Object.entries(columnValues).every(([columnName, value]) => queryRef.valueRef.current[columnName] === value)
|
|
164
161
|
) {
|
|
165
162
|
return
|
|
166
163
|
}
|
|
@@ -171,9 +168,9 @@ export const useRow: {
|
|
|
171
168
|
|
|
172
169
|
return setState as any
|
|
173
170
|
}
|
|
174
|
-
}, [id,
|
|
171
|
+
}, [id, queryRef.valueRef, sqliteTableDef.columns, store, table])
|
|
175
172
|
|
|
176
|
-
return [
|
|
173
|
+
return [queryRef.valueRef.current, setState, queryRef.queryRcRef.value]
|
|
177
174
|
}
|
|
178
175
|
|
|
179
176
|
export type Dispatch<A> = (action: A) => void
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import * as LiveStore from '@livestore/livestore'
|
|
2
|
-
import { queryDb } from '@livestore/livestore'
|
|
3
|
-
import { Effect, Schema } from '@livestore/utils/effect'
|
|
4
|
-
import { render, renderHook } from '@testing-library/react'
|
|
5
|
-
import React from 'react'
|
|
6
|
-
// @ts-expect-error no types
|
|
7
|
-
import * as ReactWindow from 'react-window'
|
|
8
|
-
import { describe, expect, it } from 'vitest'
|
|
9
|
-
|
|
10
|
-
import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
|
|
11
|
-
import * as LiveStoreReact from './mod.js'
|
|
12
|
-
|
|
13
|
-
describe('useScopedQuery', () => {
|
|
14
|
-
it('simple', () =>
|
|
15
|
-
Effect.gen(function* () {
|
|
16
|
-
const { wrapper, store, makeRenderCount } = yield* makeTodoMvcReact()
|
|
17
|
-
|
|
18
|
-
const renderCount = makeRenderCount()
|
|
19
|
-
|
|
20
|
-
store.mutate(
|
|
21
|
-
todos.insert({ id: 't1', text: 'buy milk', completed: false }),
|
|
22
|
-
todos.insert({ id: 't2', text: 'buy bread', completed: false }),
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const queryMap = new Map<string, LiveStore.LiveQuery<any>>()
|
|
26
|
-
|
|
27
|
-
const { rerender, result, unmount } = renderHook(
|
|
28
|
-
(id: string) => {
|
|
29
|
-
renderCount.inc()
|
|
30
|
-
|
|
31
|
-
return LiveStoreReact.useScopedQuery(() => {
|
|
32
|
-
const query$ = queryDb({
|
|
33
|
-
query: `select * from todos where id = '${id}'`,
|
|
34
|
-
schema: Schema.Array(tables.todos.schema),
|
|
35
|
-
})
|
|
36
|
-
queryMap.set(id, query$)
|
|
37
|
-
return query$
|
|
38
|
-
}, id)
|
|
39
|
-
},
|
|
40
|
-
{ wrapper, initialProps: 't1' },
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
expect(result.current.length).toBe(1)
|
|
44
|
-
expect(result.current[0]!.text).toBe('buy milk')
|
|
45
|
-
expect(renderCount.val).toBe(1)
|
|
46
|
-
expect(queryMap.get('t1')!.runs).toBe(1)
|
|
47
|
-
|
|
48
|
-
rerender('t2')
|
|
49
|
-
|
|
50
|
-
expect(result.current.length).toBe(1)
|
|
51
|
-
expect(result.current[0]!.text).toBe('buy bread')
|
|
52
|
-
expect(renderCount.val).toBe(2)
|
|
53
|
-
expect(queryMap.get('t1')!.runs).toBe(1)
|
|
54
|
-
expect(queryMap.get('t2')!.runs).toBe(1)
|
|
55
|
-
|
|
56
|
-
unmount()
|
|
57
|
-
|
|
58
|
-
expect(queryMap.get('t2')!.runs).toBe(1)
|
|
59
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
60
|
-
|
|
61
|
-
// NOTE this test covers some special react lifecyle paths which I couldn't easily reproduce without react-window
|
|
62
|
-
// it basically causes a "query swap" in the `useMemo` and both a `useEffect` cleanup call.
|
|
63
|
-
// To handle this properly we introduced the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`.
|
|
64
|
-
it('should work for a list with react-window', () =>
|
|
65
|
-
Effect.gen(function* () {
|
|
66
|
-
const { wrapper } = yield* makeTodoMvcReact()
|
|
67
|
-
|
|
68
|
-
const ListWrapper: React.FC<{ numItems: number }> = ({ numItems }) => {
|
|
69
|
-
return (
|
|
70
|
-
<ReactWindow.FixedSizeList
|
|
71
|
-
height={100}
|
|
72
|
-
width={100}
|
|
73
|
-
itemSize={10}
|
|
74
|
-
itemCount={numItems}
|
|
75
|
-
itemData={Array.from({ length: numItems }, (_, i) => i).reverse()}
|
|
76
|
-
>
|
|
77
|
-
{ListItem}
|
|
78
|
-
</ReactWindow.FixedSizeList>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const ListItem: React.FC<{ data: ReadonlyArray<number>; index: number }> = ({ data: ids, index }) => {
|
|
83
|
-
const id = ids[index]!
|
|
84
|
-
const res = LiveStoreReact.useScopedQuery(() => LiveStore.computed(() => id, { label: `ListItem.${id}` }), id)
|
|
85
|
-
return <div role="listitem">{res}</div>
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const renderResult = render(<ListWrapper numItems={1} />, { wrapper })
|
|
89
|
-
|
|
90
|
-
expect(renderResult.container.textContent).toBe('0')
|
|
91
|
-
|
|
92
|
-
renderResult.rerender(<ListWrapper numItems={2} />)
|
|
93
|
-
|
|
94
|
-
expect(renderResult.container.textContent).toBe('10')
|
|
95
|
-
}).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
|
|
96
|
-
})
|