@livestore/react 0.2.0 → 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.
Files changed (87) 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 +6 -4
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +47 -45
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/LiveStoreProvider.test.js +8 -2
  11. package/dist/LiveStoreProvider.test.js.map +1 -1
  12. package/dist/__tests__/fixture.d.ts +7 -10
  13. package/dist/__tests__/fixture.d.ts.map +1 -1
  14. package/dist/__tests__/fixture.js +10 -15
  15. package/dist/__tests__/fixture.js.map +1 -1
  16. package/dist/experimental/components/LiveList.d.ts +2 -2
  17. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  18. package/dist/experimental/components/LiveList.js +5 -4
  19. package/dist/experimental/components/LiveList.js.map +1 -1
  20. package/dist/mod.d.ts +0 -1
  21. package/dist/mod.d.ts.map +1 -1
  22. package/dist/mod.js +0 -1
  23. package/dist/mod.js.map +1 -1
  24. package/dist/useAtom.d.ts +4 -2
  25. package/dist/useAtom.d.ts.map +1 -1
  26. package/dist/useAtom.js +32 -28
  27. package/dist/useAtom.js.map +1 -1
  28. package/dist/useQuery.d.ts +26 -3
  29. package/dist/useQuery.d.ts.map +1 -1
  30. package/dist/useQuery.js +60 -45
  31. package/dist/useQuery.js.map +1 -1
  32. package/dist/useQuery.test.js +70 -16
  33. package/dist/useQuery.test.js.map +1 -1
  34. package/dist/useRcRef.d.ts +72 -0
  35. package/dist/useRcRef.d.ts.map +1 -0
  36. package/dist/useRcRef.js +146 -0
  37. package/dist/useRcRef.js.map +1 -0
  38. package/dist/useRcRef.test.d.ts +2 -0
  39. package/dist/useRcRef.test.d.ts.map +1 -0
  40. package/dist/useRcRef.test.js +128 -0
  41. package/dist/useRcRef.test.js.map +1 -0
  42. package/dist/useRcResource.d.ts +76 -0
  43. package/dist/useRcResource.d.ts.map +1 -0
  44. package/dist/useRcResource.js +150 -0
  45. package/dist/useRcResource.js.map +1 -0
  46. package/dist/useRcResource.test.d.ts +2 -0
  47. package/dist/useRcResource.test.d.ts.map +1 -0
  48. package/dist/useRcResource.test.js +122 -0
  49. package/dist/useRcResource.test.js.map +1 -0
  50. package/dist/useRow.d.ts +10 -7
  51. package/dist/useRow.d.ts.map +1 -1
  52. package/dist/useRow.js +16 -19
  53. package/dist/useRow.js.map +1 -1
  54. package/dist/useRow.test.js +74 -97
  55. package/dist/useRow.test.js.map +1 -1
  56. package/dist/useScopedQuery.d.ts +10 -4
  57. package/dist/useScopedQuery.d.ts.map +1 -1
  58. package/dist/useScopedQuery.js +97 -52
  59. package/dist/useScopedQuery.js.map +1 -1
  60. package/dist/useScopedQuery.test.js +13 -12
  61. package/dist/useScopedQuery.test.js.map +1 -1
  62. package/dist/utils/useStateRefWithReactiveInput.d.ts +1 -1
  63. package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  64. package/dist/utils/useStateRefWithReactiveInput.js.map +1 -1
  65. package/package.json +18 -17
  66. package/src/LiveStoreContext.ts +10 -6
  67. package/src/LiveStoreProvider.test.tsx +13 -2
  68. package/src/LiveStoreProvider.tsx +69 -53
  69. package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
  70. package/src/__snapshots__/useRow.test.tsx.snap +347 -151
  71. package/src/__tests__/fixture.tsx +11 -19
  72. package/src/experimental/components/LiveList.tsx +8 -7
  73. package/src/mod.ts +0 -1
  74. package/src/useAtom.ts +22 -11
  75. package/src/useQuery.test.tsx +165 -67
  76. package/src/useQuery.ts +84 -54
  77. package/src/useRcResource.test.tsx +167 -0
  78. package/src/useRcResource.ts +180 -0
  79. package/src/useRow.test.tsx +130 -164
  80. package/src/useRow.ts +32 -35
  81. package/src/utils/useStateRefWithReactiveInput.ts +1 -1
  82. package/dist/useTemporaryQuery.d.ts +0 -22
  83. package/dist/useTemporaryQuery.d.ts.map +0 -1
  84. package/dist/useTemporaryQuery.js +0 -75
  85. package/dist/useTemporaryQuery.js.map +0 -1
  86. package/src/useScopedQuery.test.tsx +0 -96
  87. package/src/useScopedQuery.ts +0 -142
@@ -1,30 +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'
14
+
15
+ // const strictMode = process.env.REACT_STRICT_MODE !== undefined
12
16
 
13
17
  // 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
- })
18
+ Vitest.describe('useRow', () => {
19
+ beforeEach(() => {
20
+ __resetUseRcResourceCache()
21
+ })
20
22
 
21
- 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({})
22
26
 
23
- const { result, rerender } = renderHook(
27
+ const { result, rerender } = ReactTesting.renderHook(
24
28
  (userId: string) => {
25
29
  renderCount.inc()
26
30
 
27
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
31
+ const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
28
32
  return { state, setState }
29
33
  },
30
34
  { wrapper, initialProps: 'u1' },
@@ -33,37 +37,29 @@ describe('useRow', () => {
33
37
  expect(result.current.state.id).toBe('u1')
34
38
  expect(result.current.state.username).toBe('')
35
39
  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
- )
40
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
41
+ store.mutate(tables.userInfo.insert({ id: 'u2', username: 'username_u2' }))
44
42
 
45
43
  rerender('u2')
46
44
 
45
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
47
46
  expect(result.current.state.id).toBe('u2')
48
47
  expect(result.current.state.username).toBe('username_u2')
49
48
  expect(renderCount.val).toBe(2)
50
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
49
+ }),
50
+ )
51
51
 
52
52
  // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
53
53
 
54
- it('should update the data reactively - via setState', () =>
54
+ Vitest.scopedLive('should update the data reactively - via setState', () =>
55
55
  Effect.gen(function* () {
56
- const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
57
- useGlobalReactivityGraph: false,
58
- })
59
-
60
- const renderCount = makeRenderCount()
56
+ const { wrapper, renderCount } = yield* makeTodoMvcReact({})
61
57
 
62
- const { result } = renderHook(
58
+ const { result } = ReactTesting.renderHook(
63
59
  (userId: string) => {
64
60
  renderCount.inc()
65
61
 
66
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
62
+ const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
67
63
  return { state, setState }
68
64
  },
69
65
  { wrapper, initialProps: 'u1' },
@@ -73,26 +69,23 @@ describe('useRow', () => {
73
69
  expect(result.current.state.username).toBe('')
74
70
  expect(renderCount.val).toBe(1)
75
71
 
76
- React.act(() => result.current.setState.username('username_u1_hello'))
72
+ ReactTesting.act(() => result.current.setState.username('username_u1_hello'))
77
73
 
78
74
  expect(result.current.state.id).toBe('u1')
79
75
  expect(result.current.state.username).toBe('username_u1_hello')
80
76
  expect(renderCount.val).toBe(2)
81
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
77
+ }),
78
+ )
82
79
 
83
- it('should update the data reactively - via raw store mutation', () =>
80
+ Vitest.scopedLive('should update the data reactively - via raw store mutation', () =>
84
81
  Effect.gen(function* () {
85
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
86
- useGlobalReactivityGraph: false,
87
- })
82
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
88
83
 
89
- const renderCount = makeRenderCount()
90
-
91
- const { result } = renderHook(
84
+ const { result } = ReactTesting.renderHook(
92
85
  (userId: string) => {
93
86
  renderCount.inc()
94
87
 
95
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
88
+ const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
96
89
  return { state, setState }
97
90
  },
98
91
  { wrapper, initialProps: 'u1' },
@@ -102,36 +95,30 @@ describe('useRow', () => {
102
95
  expect(result.current.state.username).toBe('')
103
96
  expect(renderCount.val).toBe(1)
104
97
 
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
- ),
98
+ ReactTesting.act(() =>
99
+ store.mutate(tables.userInfo.update({ where: { id: 'u1' }, values: { username: 'username_u1_hello' } })),
111
100
  )
112
101
 
113
102
  expect(result.current.state.id).toBe('u1')
114
103
  expect(result.current.state.username).toBe('username_u1_hello')
115
104
  expect(renderCount.val).toBe(2)
116
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
105
+ }),
106
+ )
117
107
 
118
- it('should work for a larger app', () =>
108
+ Vitest.scopedLive('should work for a larger app', () =>
119
109
  Effect.gen(function* () {
120
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
121
- useGlobalReactivityGraph: false,
122
- })
110
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
123
111
 
124
112
  const allTodos$ = LiveStore.queryDb(
125
113
  { query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
126
- { label: 'allTodos', reactivityGraph },
114
+ { label: 'allTodos' },
127
115
  )
128
116
 
129
- const appRouterRenderCount = makeRenderCount()
130
117
  let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
131
118
  const AppRouter: React.FC = () => {
132
- appRouterRenderCount.inc()
119
+ renderCount.inc()
133
120
 
134
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
121
+ const [state, setState] = LiveStoreReact.useRow(AppRouterSchema)
135
122
 
136
123
  globalSetState = setState
137
124
 
@@ -159,15 +146,15 @@ describe('useRow', () => {
159
146
  }
160
147
 
161
148
  const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
162
- const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
149
+ const [todo] = LiveStoreReact.useRow(todos, id)
163
150
  return <div role="content">{JSON.stringify(todo)}</div>
164
151
  }
165
152
 
166
- const renderResult = render(<AppRouter />, { wrapper })
153
+ const renderResult = ReactTesting.render(<AppRouter />, { wrapper })
167
154
 
168
- expect(appRouterRenderCount.val).toBe(1)
155
+ expect(renderCount.val).toBe(1)
169
156
 
170
- React.act(() =>
157
+ ReactTesting.act(() =>
171
158
  store.mutate(
172
159
  LiveStore.rawSqlMutation({
173
160
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
@@ -175,19 +162,19 @@ describe('useRow', () => {
175
162
  ),
176
163
  )
177
164
 
178
- expect(appRouterRenderCount.val).toBe(1)
165
+ expect(renderCount.val).toBe(1)
179
166
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
180
167
 
181
- React.act(() => globalSetState!.currentTaskId('t1'))
168
+ ReactTesting.act(() => globalSetState!.currentTaskId('t1'))
182
169
 
183
- expect(appRouterRenderCount.val).toBe(2)
170
+ expect(renderCount.val).toBe(2)
184
171
  expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
185
172
  `"{"id":"t1","text":"buy milk","completed":false}"`,
186
173
  )
187
174
 
188
175
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
189
176
 
190
- React.act(() =>
177
+ ReactTesting.act(() =>
191
178
  store.mutate(
192
179
  LiveStore.rawSqlMutation({
193
180
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
@@ -199,37 +186,33 @@ describe('useRow', () => {
199
186
  ),
200
187
  )
201
188
 
202
- expect(appRouterRenderCount.val).toBe(3)
189
+ expect(renderCount.val).toBe(3)
203
190
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
204
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
191
+ }),
192
+ )
205
193
 
206
- 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', () =>
207
195
  Effect.gen(function* () {
208
- const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
209
- useGlobalReactivityGraph: false,
210
- })
211
- const renderCount = makeRenderCount()
196
+ const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
212
197
 
213
198
  store.mutate(
214
199
  todos.insert({ id: 't1', text: 'buy milk', completed: false }),
215
200
  todos.insert({ id: 't2', text: 'buy bread', completed: false }),
216
201
  )
217
202
 
218
- const { result, unmount, rerender } = renderHook(
203
+ const { result, unmount, rerender } = ReactTesting.renderHook(
219
204
  (userId: string) => {
220
205
  renderCount.inc()
221
206
 
222
- const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
223
- const todos = LiveStoreReact.useScopedQuery(
224
- () =>
225
- LiveStore.queryDb(
226
- (get) => ({
227
- query: LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
228
- schema: Schema.Array(tables.todos.schema),
229
- }),
230
- { reactivityGraph, label: 'todosFiltered' },
231
- ),
232
- 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
233
216
  )
234
217
 
235
218
  return { todos }
@@ -237,16 +220,9 @@ describe('useRow', () => {
237
220
  { wrapper, initialProps: 'u1' },
238
221
  )
239
222
 
240
- React.act(() =>
241
- store.mutate(
242
- LiveStore.rawSqlMutation({
243
- sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
244
- }),
245
- ),
246
- )
223
+ ReactTesting.act(() => store.mutate(tables.userInfo.insert({ id: 'u2', username: 'username_u2', text: 'milk' })))
247
224
 
248
225
  expect(result.current.todos.length).toBe(2)
249
- // expect(result.current.state.username).toBe('')
250
226
  expect(renderCount.val).toBe(1)
251
227
 
252
228
  rerender('u2')
@@ -255,90 +231,80 @@ describe('useRow', () => {
255
231
  expect(renderCount.val).toBe(2)
256
232
 
257
233
  unmount()
258
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
259
-
260
- let cachedProvider: BasicTracerProvider | undefined
234
+ }),
235
+ )
261
236
 
262
- describe('otel', () => {
263
- const exporter = new InMemorySpanExporter()
264
-
265
- const provider = cachedProvider ?? new BasicTracerProvider()
266
- cachedProvider = provider
267
- provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
237
+ Vitest.describe('otel', () => {
238
+ const provider = new BasicTracerProvider({})
268
239
  provider.register()
269
240
 
270
- const otelTracer = otel.trace.getTracer('test')
271
-
272
- const span = otelTracer.startSpan('test')
273
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
241
+ it.each([{ strictMode: true }, { strictMode: false }])(
242
+ 'should update the data based on component key strictMode=%s',
243
+ async ({ strictMode }) => {
244
+ const exporter = new InMemorySpanExporter()
274
245
 
275
- it('should update the data based on component key', async () => {
276
- const { strictMode } = await Effect.gen(function* () {
277
- const { wrapper, store, reactivityGraph, makeRenderCount, strictMode } = yield* makeTodoMvcReact({
278
- useGlobalReactivityGraph: false,
279
- otelContext,
280
- otelTracer,
281
- })
246
+ // const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
247
+ provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
282
248
 
283
- const renderCount = makeRenderCount()
249
+ const otelTracer = otel.trace.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
284
250
 
285
- const { result, rerender, unmount } = renderHook(
286
- (userId: string) => {
287
- renderCount.inc()
288
-
289
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
290
- return { state, setState }
291
- },
292
- { wrapper, initialProps: 'u1' },
293
- )
251
+ const span = otelTracer.startSpan('test-root')
252
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
294
253
 
295
- expect(result.current.state.id).toBe('u1')
296
- expect(result.current.state.username).toBe('')
297
- expect(renderCount.val).toBe(1)
254
+ await Effect.gen(function* () {
255
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({
256
+ otelContext,
257
+ otelTracer,
258
+ strictMode,
259
+ })
298
260
 
299
- React.act(() =>
300
- store.mutate(
301
- LiveStore.rawSqlMutation({
302
- sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
303
- }),
304
- ),
305
- )
261
+ const { result, rerender, unmount } = ReactTesting.renderHook(
262
+ (userId: string) => {
263
+ renderCount.inc()
306
264
 
307
- rerender('u2')
308
-
309
- expect(result.current.state.id).toBe('u2')
310
- expect(result.current.state.username).toBe('username_u2')
311
- expect(renderCount.val).toBe(2)
312
-
313
- unmount()
314
- span.end()
315
-
316
- return { strictMode }
317
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
318
-
319
- const mapAttributes = (attributes: otel.Attributes) => {
320
- return ReadonlyRecord.map(attributes, (val, key) => {
321
- if (key === 'stackInfo') {
322
- const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
323
- // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
324
- stackInfo.frames.forEach((_) => {
325
- if (_.name.includes('renderHook.wrapper')) {
326
- _.name = 'renderHook.wrapper'
327
- }
328
- _.filePath = '__REPLACED_FOR_SNAPSHOT__'
329
- })
330
- return JSON.stringify(stackInfo)
331
- }
332
- return val
333
- })
334
- }
265
+ const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
266
+ return { state, setState }
267
+ },
268
+ { wrapper, initialProps: 'u1' },
269
+ )
335
270
 
336
- // TODO improve testing setup so "obsolete" warning is avoided
337
- if (strictMode) {
338
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=true')
339
- } else {
340
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=false')
341
- }
342
- })
271
+ expect(result.current.state.id).toBe('u1')
272
+ expect(result.current.state.username).toBe('')
273
+ expect(renderCount.val).toBe(1)
274
+
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' })))
278
+
279
+ rerender('u2')
280
+
281
+ expect(result.current.state.id).toBe('u2')
282
+ expect(result.current.state.username).toBe('username_u2')
283
+ expect(renderCount.val).toBe(2)
284
+
285
+ unmount()
286
+ span.end()
287
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
288
+
289
+ const mapAttributes = (attributes: otel.Attributes) => {
290
+ return ReadonlyRecord.map(attributes, (val, key) => {
291
+ if (key === 'firstStackInfo') {
292
+ const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
293
+ // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
294
+ stackInfo.frames.forEach((_) => {
295
+ if (_.name.includes('renderHook.wrapper')) {
296
+ _.name = 'renderHook.wrapper'
297
+ }
298
+ _.filePath = '__REPLACED_FOR_SNAPSHOT__'
299
+ })
300
+ return JSON.stringify(stackInfo)
301
+ }
302
+ return val
303
+ })
304
+ }
305
+
306
+ expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
307
+ },
308
+ )
343
309
  })
344
310
  })
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,
@@ -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?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
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: UseRowOptionsBase & RowQuery.RequiredColumnsOptions<TTableDef>,
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 | UseRowOptionsBase,
78
- options_?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
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: (UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>) | undefined =
82
+ const options: (Partial<RowQuery.RequiredColumnsOptions<TTableDef>> & { store?: Store }) | undefined =
83
83
  typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? options_ : idOrOptions
84
- const { insertValues, reactivityGraph } = options ?? {}
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 Query$ = LiveQuery<RowQuery.Result<TTableDef>, QueryInfo.Row>
109
- const { query$, otelContext } = useMakeScopedQuery(
110
- (otelContext) =>
108
+ type QueryDef = LiveQueryDef<RowQuery.Result<TTableDef>, QueryInfo.Row>
109
+ const queryDef: QueryDef = React.useMemo(
110
+ () =>
111
111
  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
- },
112
+ ? queryDb(rowQuery(), {})
113
+ : queryDb(rowQuery(id!, { insertValues: insertValues! }), { deps: idStr! }),
114
+ [id, insertValues, rowQuery, table, idStr],
121
115
  )
122
116
 
123
- const query$Ref = useQueryRef(query$, otelContext) as React.MutableRefObject<RowQuery.Result<TTableDef>>
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(query$Ref.current) : newValueOrFn
129
- if (query$Ref.current === newValue) return
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(query$Ref.current[columnName]) : 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 (query$Ref.current[columnName] === newValue) return
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(query$Ref.current) : 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]) => query$Ref.current[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, query$Ref, sqliteTableDef.columns, store, table])
171
+ }, [id, queryRef.valueRef, sqliteTableDef.columns, store, table])
175
172
 
176
- return [query$Ref.current, setState, query$]
173
+ return [queryRef.valueRef.current, setState, queryRef.queryRcRef.value]
177
174
  }
178
175
 
179
176
  export type Dispatch<A> = (action: A) => void
@@ -12,7 +12,7 @@ import React from 'react'
12
12
  */
13
13
  export const useStateRefWithReactiveInput = <T>(
14
14
  inputState: T,
15
- ): [React.MutableRefObject<T>, (newState: T | ((prev: T) => T)) => void] => {
15
+ ): [React.RefObject<T>, (newState: T | ((prev: T) => T)) => void] => {
16
16
  const [_, rerender] = React.useState(0)
17
17
 
18
18
  const lastKnownInputStateRef = React.useRef<T>(inputState)
@@ -1,22 +0,0 @@
1
- import type { QueryInfo } from '@livestore/common';
2
- import type { LiveQuery } from '@livestore/livestore';
3
- import * as otel from '@opentelemetry/api';
4
- import React from 'react';
5
- export type DepKey = string | number | ReadonlyArray<string | number>;
6
- /**
7
- * Creates a query, subscribes and destroys it when the component unmounts.
8
- *
9
- * The `key` is used to determine whether the a new query should be created or if the existing one should be reused.
10
- */
11
- export declare const useScopedQuery: <TResult>(makeQuery: () => LiveQuery<TResult>, key: DepKey) => TResult;
12
- export declare const useScopedQueryRef: <TResult>(makeQuery: () => LiveQuery<TResult>, key: DepKey) => React.MutableRefObject<TResult>;
13
- export declare const useMakeScopedQuery: <TResult, TQueryInfo extends QueryInfo>(makeQuery: (otelContext: otel.Context) => LiveQuery<TResult, TQueryInfo>, key: DepKey, options?: {
14
- otel?: {
15
- spanName?: string;
16
- attributes?: otel.Attributes;
17
- };
18
- }) => {
19
- query$: LiveQuery<TResult, TQueryInfo>;
20
- otelContext: otel.Context;
21
- };
22
- //# sourceMappingURL=useTemporaryQuery.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useTemporaryQuery.d.ts","sourceRoot":"","sources":["../src/useTemporaryQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,KAAK,IAAI,MAAM,oBAAoB,CAAA;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAA;AAuBzB,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;AAErE;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,aAAa,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,MAAM,KAAG,OAChD,CAAA;AAE3C,eAAO,MAAM,iBAAiB,GAAI,OAAO,aAC5B,MAAM,SAAS,CAAC,OAAO,CAAC,OAC9B,MAAM,KACV,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAIhC,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,OAAO,EAAE,UAAU,SAAS,SAAS,aAC3D,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,OACnE,MAAM,YACD;IACR,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,CAAA;KAC7B,CAAA;CACF,KACA;IAAE,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAA;CA4ErE,CAAA"}