@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.
Files changed (76) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +5 -3
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +7 -3
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +5 -2
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +2 -17
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/__tests__/fixture.d.ts +6 -8
  11. package/dist/__tests__/fixture.d.ts.map +1 -1
  12. package/dist/__tests__/fixture.js +6 -7
  13. package/dist/__tests__/fixture.js.map +1 -1
  14. package/dist/experimental/components/LiveList.d.ts +2 -2
  15. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  16. package/dist/experimental/components/LiveList.js +5 -4
  17. package/dist/experimental/components/LiveList.js.map +1 -1
  18. package/dist/mod.d.ts +0 -1
  19. package/dist/mod.d.ts.map +1 -1
  20. package/dist/mod.js +0 -1
  21. package/dist/mod.js.map +1 -1
  22. package/dist/useAtom.d.ts +4 -2
  23. package/dist/useAtom.d.ts.map +1 -1
  24. package/dist/useAtom.js +32 -28
  25. package/dist/useAtom.js.map +1 -1
  26. package/dist/useQuery.d.ts +26 -3
  27. package/dist/useQuery.d.ts.map +1 -1
  28. package/dist/useQuery.js +60 -45
  29. package/dist/useQuery.js.map +1 -1
  30. package/dist/useQuery.test.js +70 -16
  31. package/dist/useQuery.test.js.map +1 -1
  32. package/dist/useRcRef.d.ts +72 -0
  33. package/dist/useRcRef.d.ts.map +1 -0
  34. package/dist/useRcRef.js +146 -0
  35. package/dist/useRcRef.js.map +1 -0
  36. package/dist/useRcRef.test.d.ts +2 -0
  37. package/dist/useRcRef.test.d.ts.map +1 -0
  38. package/dist/useRcRef.test.js +128 -0
  39. package/dist/useRcRef.test.js.map +1 -0
  40. package/dist/useRcResource.d.ts +76 -0
  41. package/dist/useRcResource.d.ts.map +1 -0
  42. package/dist/useRcResource.js +150 -0
  43. package/dist/useRcResource.js.map +1 -0
  44. package/dist/useRcResource.test.d.ts +2 -0
  45. package/dist/useRcResource.test.d.ts.map +1 -0
  46. package/dist/useRcResource.test.js +122 -0
  47. package/dist/useRcResource.test.js.map +1 -0
  48. package/dist/useRow.d.ts +10 -7
  49. package/dist/useRow.d.ts.map +1 -1
  50. package/dist/useRow.js +23 -22
  51. package/dist/useRow.js.map +1 -1
  52. package/dist/useRow.test.js +62 -80
  53. package/dist/useRow.test.js.map +1 -1
  54. package/dist/useScopedQuery.d.ts +10 -4
  55. package/dist/useScopedQuery.d.ts.map +1 -1
  56. package/dist/useScopedQuery.js +96 -52
  57. package/dist/useScopedQuery.js.map +1 -1
  58. package/dist/useScopedQuery.test.js +13 -12
  59. package/dist/useScopedQuery.test.js.map +1 -1
  60. package/package.json +6 -6
  61. package/src/LiveStoreContext.ts +10 -6
  62. package/src/LiveStoreProvider.tsx +3 -19
  63. package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
  64. package/src/__snapshots__/useRow.test.tsx.snap +335 -142
  65. package/src/__tests__/fixture.tsx +6 -9
  66. package/src/experimental/components/LiveList.tsx +8 -7
  67. package/src/mod.ts +0 -1
  68. package/src/useAtom.ts +22 -11
  69. package/src/useQuery.test.tsx +165 -67
  70. package/src/useQuery.ts +84 -54
  71. package/src/useRcResource.test.tsx +167 -0
  72. package/src/useRcResource.ts +180 -0
  73. package/src/useRow.test.tsx +73 -107
  74. package/src/useRow.ts +42 -40
  75. package/src/useScopedQuery.test.tsx +0 -96
  76. package/src/useScopedQuery.ts +0 -143
@@ -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 { render, renderHook } from '@testing-library/react'
7
+ import * as ReactTesting from '@testing-library/react'
7
8
  import React from 'react'
8
- import { describe, expect, it } from 'vitest'
9
+ import { beforeEach, expect, it } from 'vitest'
9
10
 
10
- import { AppComponentSchema, AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
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
- it('should update the data based on component key', () =>
18
- Effect.gen(function* () {
19
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
20
- useGlobalReactivityGraph: false,
21
- })
18
+ Vitest.describe('useRow', () => {
19
+ beforeEach(() => {
20
+ __resetUseRcResourceCache()
21
+ })
22
22
 
23
- const renderCount = makeRenderCount()
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(AppComponentSchema, userId, { reactivityGraph })
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
- React.act(() =>
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
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
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
- it('should update the data reactively - via setState', () =>
54
+ Vitest.scopedLive('should update the data reactively - via setState', () =>
57
55
  Effect.gen(function* () {
58
- const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
59
- useGlobalReactivityGraph: false,
60
- })
56
+ const { wrapper, renderCount } = yield* makeTodoMvcReact({})
61
57
 
62
- const renderCount = makeRenderCount()
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(AppComponentSchema, userId, { reactivityGraph })
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
- React.act(() => result.current.setState.username('username_u1_hello'))
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
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
77
+ }),
78
+ )
84
79
 
85
- it('should update the data reactively - via raw store mutation', () =>
80
+ Vitest.scopedLive('should update the data reactively - via raw store mutation', () =>
86
81
  Effect.gen(function* () {
87
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
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(AppComponentSchema, userId, { reactivityGraph })
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
- React.act(() =>
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
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
105
+ }),
106
+ )
119
107
 
120
- it('should work for a larger app', () =>
108
+ Vitest.scopedLive('should work for a larger app', () =>
121
109
  Effect.gen(function* () {
122
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
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', reactivityGraph },
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
- appRouterRenderCount.inc()
119
+ renderCount.inc()
135
120
 
136
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
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, { reactivityGraph })
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(appRouterRenderCount.val).toBe(1)
155
+ expect(renderCount.val).toBe(1)
171
156
 
172
- React.act(() =>
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(appRouterRenderCount.val).toBe(1)
165
+ expect(renderCount.val).toBe(1)
181
166
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
182
167
 
183
- React.act(() => globalSetState!.currentTaskId('t1'))
168
+ ReactTesting.act(() => globalSetState!.currentTaskId('t1'))
184
169
 
185
- expect(appRouterRenderCount.val).toBe(2)
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
- React.act(() =>
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(appRouterRenderCount.val).toBe(3)
189
+ expect(renderCount.val).toBe(3)
205
190
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
206
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
191
+ }),
192
+ )
207
193
 
208
- it('should work for a useRow query chained with a useTemporary query', () =>
194
+ Vitest.scopedLive('should work for a useRow query chained with a useTemporary query', () =>
209
195
  Effect.gen(function* () {
210
- const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
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(AppComponentSchema, userId, { reactivityGraph })
225
- const todos = LiveStoreReact.useScopedQuery(
226
- () =>
227
- LiveStore.queryDb(
228
- (get) => ({
229
- query: LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
230
- schema: Schema.Array(tables.todos.schema),
231
- }),
232
- { reactivityGraph, label: 'todosFiltered' },
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
- 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
- )
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
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
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, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
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 renderCount = makeRenderCount()
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(AppComponentSchema, userId, { reactivityGraph })
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
- React.act(() =>
304
- store.mutate(
305
- LiveStore.rawSqlMutation({
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 === 'stackInfo') {
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, ReactivityGraph } from '@livestore/livestore'
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?: UseRowOptionsBase,
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,
@@ -53,9 +51,11 @@ export const useRow: {
53
51
  >(
54
52
  table: TTableDef,
55
53
  // TODO adjust so it works with arbitrary primary keys or unique constraints
56
- id: string | SessionIdSymbol,
57
- options?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
54
+ id: string | SessionIdSymbol | number,
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,
@@ -64,8 +64,8 @@ export const useRow: {
64
64
  >(
65
65
  table: TTableDef,
66
66
  // TODO adjust so it works with arbitrary primary keys or unique constraints
67
- id: string | SessionIdSymbol,
68
- options: UseRowOptionsBase & RowQuery.RequiredColumnsOptions<TTableDef>,
67
+ id: string | SessionIdSymbol | number,
68
+ options: RowQuery.RequiredColumnsOptions<TTableDef> & { store?: Store },
69
69
  ): UseRowResult<TTableDef>
70
70
  } = <
71
71
  TTableDef extends DbSchema.TableDef<
@@ -74,14 +74,19 @@ export const useRow: {
74
74
  >,
75
75
  >(
76
76
  table: TTableDef,
77
- idOrOptions?: string | SessionIdSymbol | UseRowOptionsBase,
78
- options_?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
77
+ idOrOptions?: string | SessionIdSymbol | number | { store?: Store },
78
+ options_?: Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store },
79
79
  ): UseRowResult<TTableDef> => {
80
80
  const sqliteTableDef = table.sqliteDef
81
- const id = typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? idOrOptions : undefined
82
- const options: (UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>) | undefined =
83
- typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? options_ : idOrOptions
84
- const { insertValues, reactivityGraph } = options ?? {}
81
+ const id =
82
+ typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol || typeof idOrOptions === 'number'
83
+ ? idOrOptions
84
+ : undefined
85
+ const options: (Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store }) | undefined =
86
+ typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol || typeof idOrOptions === 'number'
87
+ ? options_
88
+ : idOrOptions
89
+ const { insertValues } = options ?? {}
85
90
 
86
91
  type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
87
92
 
@@ -91,7 +96,7 @@ export const useRow: {
91
96
  shouldNeverHappen(`useRow called on table "${tableName}" which does not have 'deriveMutations: true' set`)
92
97
  }
93
98
 
94
- const { store } = useStore()
99
+ const { store } = useStore({ store: options?.store })
95
100
 
96
101
  if (
97
102
  store.schema.tables.has(table.sqliteDef.name) === false &&
@@ -102,31 +107,28 @@ export const useRow: {
102
107
 
103
108
  // console.debug('useRow', tableName, id)
104
109
 
105
- const idStr = id === SessionIdSymbol ? 'session' : id
110
+ const idVal = id === SessionIdSymbol ? 'session' : id
106
111
  const rowQuery = table.query.row as any
107
112
 
108
- type Query$ = LiveQuery<RowQuery.Result<TTableDef>, QueryInfo.Row>
109
- const { query$, otelContext } = useMakeScopedQuery(
110
- (otelContext) =>
113
+ type QueryDef = LiveQueryDef<RowQuery.Result<TTableDef>, QueryInfo.Row>
114
+ const queryDef: QueryDef = React.useMemo(
115
+ () =>
111
116
  DbSchema.tableIsSingleton(table)
112
- ? (queryDb(rowQuery(), { reactivityGraph, otelContext }) as any as Query$)
113
- : (queryDb(rowQuery(id!, { insertValues: insertValues! }), { reactivityGraph, otelContext }) as any as Query$),
114
- [idStr!, tableName],
115
- {
116
- otel: {
117
- spanName: `LiveStore:useRow:${tableName}${idStr === undefined ? '' : `:${idStr}`}`,
118
- attributes: { id: idStr },
119
- },
120
- },
117
+ ? queryDb(rowQuery(), {})
118
+ : queryDb(rowQuery(id!, { insertValues: insertValues! }), { deps: idVal! }),
119
+ [id, insertValues, rowQuery, table, idVal],
121
120
  )
122
121
 
123
- const query$Ref = useQueryRef(query$, otelContext) as React.RefObject<RowQuery.Result<TTableDef>>
122
+ const queryRef = useQueryRef(queryDef, {
123
+ otelSpanName: `LiveStore:useRow:${tableName}${idVal === undefined ? '' : `:${idVal}`}`,
124
+ store: options?.store,
125
+ })
124
126
 
125
127
  const setState = React.useMemo<StateSetters<TTableDef>>(() => {
126
128
  if (table.options.isSingleColumn) {
127
129
  return (newValueOrFn: RowQuery.Result<TTableDef>) => {
128
- const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
129
- if (query$Ref.current === newValue) return
130
+ const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
131
+ if (queryRef.valueRef.current === newValue) return
130
132
 
131
133
  // NOTE we need to account for the short-hand syntax for single-column+singleton tables
132
134
  if (table.options.isSingleton) {
@@ -141,11 +143,11 @@ export const useRow: {
141
143
  ReadonlyRecord.map(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
142
144
  const newValue =
143
145
  // @ts-expect-error TODO fix typing
144
- typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current[columnName]) : newValueOrFn
146
+ typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current[columnName]) : newValueOrFn
145
147
 
146
148
  // Don't update the state if it's the same as the value already seen in the component
147
149
  // @ts-expect-error TODO fix typing
148
- if (query$Ref.current[columnName] === newValue) return
150
+ if (queryRef.valueRef.current[columnName] === newValue) return
149
151
 
150
152
  store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: { [columnName]: newValue } }))
151
153
  // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
@@ -154,13 +156,13 @@ export const useRow: {
154
156
  setState.setMany = (columnValuesOrFn: Partial<TComponentState>) => {
155
157
  const columnValues =
156
158
  // @ts-expect-error TODO fix typing
157
- typeof columnValuesOrFn === 'function' ? columnValuesOrFn(query$Ref.current) : columnValuesOrFn
159
+ typeof columnValuesOrFn === 'function' ? columnValuesOrFn(queryRef.valueRef.current) : columnValuesOrFn
158
160
 
159
161
  // TODO use hashing instead
160
162
  // Don't update the state if it's the same as the value already seen in the component
161
163
  if (
162
164
  // @ts-expect-error TODO fix typing
163
- Object.entries(columnValues).every(([columnName, value]) => query$Ref.current[columnName] === value)
165
+ Object.entries(columnValues).every(([columnName, value]) => queryRef.valueRef.current[columnName] === value)
164
166
  ) {
165
167
  return
166
168
  }
@@ -171,9 +173,9 @@ export const useRow: {
171
173
 
172
174
  return setState as any
173
175
  }
174
- }, [id, query$Ref, sqliteTableDef.columns, store, table])
176
+ }, [id, queryRef.valueRef, sqliteTableDef.columns, store, table])
175
177
 
176
- return [query$Ref.current, setState, query$]
178
+ return [queryRef.valueRef.current, setState, queryRef.queryRcRef.value]
177
179
  }
178
180
 
179
181
  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
- })