@livestore/react 0.3.0-dev.5 → 0.3.0-dev.51

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 (99) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +10 -4
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +1 -11
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +31 -13
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +84 -55
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/LiveStoreProvider.test.js +80 -29
  11. package/dist/LiveStoreProvider.test.js.map +1 -1
  12. package/dist/__tests__/fixture.d.ts +122 -557
  13. package/dist/__tests__/fixture.d.ts.map +1 -1
  14. package/dist/__tests__/fixture.js +71 -30
  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 +10 -6
  19. package/dist/experimental/components/LiveList.js.map +1 -1
  20. package/dist/mod.d.ts +4 -5
  21. package/dist/mod.d.ts.map +1 -1
  22. package/dist/mod.js +4 -5
  23. package/dist/mod.js.map +1 -1
  24. package/dist/useClientDocument.d.ts +61 -0
  25. package/dist/useClientDocument.d.ts.map +1 -0
  26. package/dist/useClientDocument.js +79 -0
  27. package/dist/useClientDocument.js.map +1 -0
  28. package/dist/useClientDocument.test.d.ts +2 -0
  29. package/dist/useClientDocument.test.d.ts.map +1 -0
  30. package/dist/useClientDocument.test.js +175 -0
  31. package/dist/useClientDocument.test.js.map +1 -0
  32. package/dist/useQuery.d.ts +25 -3
  33. package/dist/useQuery.d.ts.map +1 -1
  34. package/dist/useQuery.js +67 -47
  35. package/dist/useQuery.js.map +1 -1
  36. package/dist/useQuery.test.d.ts +1 -1
  37. package/dist/useQuery.test.d.ts.map +1 -1
  38. package/dist/useQuery.test.js +86 -24
  39. package/dist/useQuery.test.js.map +1 -1
  40. package/dist/useRcResource.d.ts +76 -0
  41. package/dist/useRcResource.d.ts.map +1 -0
  42. package/dist/useRcResource.js +152 -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/useStore.d.ts +9 -0
  49. package/dist/useStore.d.ts.map +1 -0
  50. package/dist/useStore.js +28 -0
  51. package/dist/useStore.js.map +1 -0
  52. package/dist/utils/useStateRefWithReactiveInput.d.ts +1 -1
  53. package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  54. package/dist/utils/useStateRefWithReactiveInput.js.map +1 -1
  55. package/package.json +26 -19
  56. package/src/LiveStoreContext.ts +11 -16
  57. package/src/LiveStoreProvider.test.tsx +176 -37
  58. package/src/LiveStoreProvider.tsx +159 -82
  59. package/src/__snapshots__/useClientDocument.test.tsx.snap +613 -0
  60. package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
  61. package/src/__tests__/fixture.tsx +75 -48
  62. package/src/experimental/components/LiveList.tsx +10 -7
  63. package/src/mod.ts +5 -6
  64. package/src/useClientDocument.test.tsx +306 -0
  65. package/src/useClientDocument.ts +157 -0
  66. package/src/useQuery.test.tsx +182 -71
  67. package/src/useQuery.ts +95 -58
  68. package/src/useRcResource.test.tsx +167 -0
  69. package/src/useRcResource.ts +182 -0
  70. package/src/useStore.ts +36 -0
  71. package/src/utils/useStateRefWithReactiveInput.ts +1 -1
  72. package/dist/useAtom.d.ts +0 -5
  73. package/dist/useAtom.d.ts.map +0 -1
  74. package/dist/useAtom.js +0 -38
  75. package/dist/useAtom.js.map +0 -1
  76. package/dist/useRow.d.ts +0 -50
  77. package/dist/useRow.d.ts.map +0 -1
  78. package/dist/useRow.js +0 -93
  79. package/dist/useRow.js.map +0 -1
  80. package/dist/useRow.test.d.ts +0 -2
  81. package/dist/useRow.test.d.ts.map +0 -1
  82. package/dist/useRow.test.js +0 -206
  83. package/dist/useRow.test.js.map +0 -1
  84. package/dist/useScopedQuery.d.ts +0 -33
  85. package/dist/useScopedQuery.d.ts.map +0 -1
  86. package/dist/useScopedQuery.js +0 -86
  87. package/dist/useScopedQuery.js.map +0 -1
  88. package/dist/useScopedQuery.test.d.ts +0 -2
  89. package/dist/useScopedQuery.test.d.ts.map +0 -1
  90. package/dist/useScopedQuery.test.js +0 -60
  91. package/dist/useScopedQuery.test.js.map +0 -1
  92. package/src/__snapshots__/useRow.test.tsx.snap +0 -367
  93. package/src/useAtom.ts +0 -52
  94. package/src/useRow.test.tsx +0 -343
  95. package/src/useRow.ts +0 -188
  96. package/src/useScopedQuery.test.tsx +0 -96
  97. package/src/useScopedQuery.ts +0 -142
  98. package/tsconfig.json +0 -20
  99. package/vitest.config.js +0 -17
@@ -1,343 +0,0 @@
1
- import * as LiveStore from '@livestore/livestore'
2
- import { getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
3
- import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
4
- import * as otel from '@opentelemetry/api'
5
- import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
6
- import { render, renderHook } from '@testing-library/react'
7
- import React from 'react'
8
- import { describe, expect, it } from 'vitest'
9
-
10
- import { AppComponentSchema, AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
11
- import * as LiveStoreReact from './mod.js'
12
-
13
- // NOTE running tests concurrently doesn't work with the default global db graph
14
- describe('useRow', () => {
15
- it('should update the data based on component key', () =>
16
- Effect.gen(function* () {
17
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
18
- useGlobalReactivityGraph: false,
19
- })
20
-
21
- const renderCount = makeRenderCount()
22
-
23
- const { result, rerender } = renderHook(
24
- (userId: string) => {
25
- renderCount.inc()
26
-
27
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
28
- return { state, setState }
29
- },
30
- { wrapper, initialProps: 'u1' },
31
- )
32
-
33
- expect(result.current.state.id).toBe('u1')
34
- expect(result.current.state.username).toBe('')
35
- expect(renderCount.val).toBe(1)
36
-
37
- React.act(() =>
38
- store.mutate(
39
- LiveStore.rawSqlMutation({
40
- sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
41
- }),
42
- ),
43
- )
44
-
45
- rerender('u2')
46
-
47
- expect(result.current.state.id).toBe('u2')
48
- expect(result.current.state.username).toBe('username_u2')
49
- expect(renderCount.val).toBe(2)
50
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
51
-
52
- // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
53
-
54
- it('should update the data reactively - via setState', () =>
55
- Effect.gen(function* () {
56
- const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
57
- useGlobalReactivityGraph: false,
58
- })
59
-
60
- const renderCount = makeRenderCount()
61
-
62
- const { result } = renderHook(
63
- (userId: string) => {
64
- renderCount.inc()
65
-
66
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
67
- return { state, setState }
68
- },
69
- { wrapper, initialProps: 'u1' },
70
- )
71
-
72
- expect(result.current.state.id).toBe('u1')
73
- expect(result.current.state.username).toBe('')
74
- expect(renderCount.val).toBe(1)
75
-
76
- React.act(() => result.current.setState.username('username_u1_hello'))
77
-
78
- expect(result.current.state.id).toBe('u1')
79
- expect(result.current.state.username).toBe('username_u1_hello')
80
- expect(renderCount.val).toBe(2)
81
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
82
-
83
- it('should update the data reactively - via raw store mutation', () =>
84
- Effect.gen(function* () {
85
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
86
- useGlobalReactivityGraph: false,
87
- })
88
-
89
- const renderCount = makeRenderCount()
90
-
91
- const { result } = renderHook(
92
- (userId: string) => {
93
- renderCount.inc()
94
-
95
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
96
- return { state, setState }
97
- },
98
- { wrapper, initialProps: 'u1' },
99
- )
100
-
101
- expect(result.current.state.id).toBe('u1')
102
- expect(result.current.state.username).toBe('')
103
- expect(renderCount.val).toBe(1)
104
-
105
- React.act(() =>
106
- store.mutate(
107
- LiveStore.rawSqlMutation({
108
- sql: LiveStore.sql`UPDATE UserInfo SET username = 'username_u1_hello' WHERE id = 'u1';`,
109
- }),
110
- ),
111
- )
112
-
113
- expect(result.current.state.id).toBe('u1')
114
- expect(result.current.state.username).toBe('username_u1_hello')
115
- expect(renderCount.val).toBe(2)
116
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
117
-
118
- it('should work for a larger app', () =>
119
- Effect.gen(function* () {
120
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
121
- useGlobalReactivityGraph: false,
122
- })
123
-
124
- const allTodos$ = LiveStore.queryDb(
125
- { query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
126
- { label: 'allTodos', reactivityGraph },
127
- )
128
-
129
- const appRouterRenderCount = makeRenderCount()
130
- let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
131
- const AppRouter: React.FC = () => {
132
- appRouterRenderCount.inc()
133
-
134
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
135
-
136
- globalSetState = setState
137
-
138
- return (
139
- <div>
140
- <TasksList setTaskId={setState.currentTaskId} />
141
- <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
142
- {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
143
- </div>
144
- )
145
- }
146
-
147
- const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
148
- const allTodos = LiveStoreReact.useQuery(allTodos$)
149
-
150
- return (
151
- <div>
152
- {allTodos.map((_) => (
153
- <div key={_.id} onClick={() => setTaskId(_.id)}>
154
- {_.id}
155
- </div>
156
- ))}
157
- </div>
158
- )
159
- }
160
-
161
- const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
162
- const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
163
- return <div role="content">{JSON.stringify(todo)}</div>
164
- }
165
-
166
- const renderResult = render(<AppRouter />, { wrapper })
167
-
168
- expect(appRouterRenderCount.val).toBe(1)
169
-
170
- React.act(() =>
171
- store.mutate(
172
- LiveStore.rawSqlMutation({
173
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
174
- }),
175
- ),
176
- )
177
-
178
- expect(appRouterRenderCount.val).toBe(1)
179
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
180
-
181
- React.act(() => globalSetState!.currentTaskId('t1'))
182
-
183
- expect(appRouterRenderCount.val).toBe(2)
184
- expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
185
- `"{"id":"t1","text":"buy milk","completed":false}"`,
186
- )
187
-
188
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
189
-
190
- React.act(() =>
191
- store.mutate(
192
- LiveStore.rawSqlMutation({
193
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
194
- }),
195
- AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
196
- LiveStore.rawSqlMutation({
197
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
198
- }),
199
- ),
200
- )
201
-
202
- expect(appRouterRenderCount.val).toBe(3)
203
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
204
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
205
-
206
- it('should work for a useRow query chained with a useTemporary query', () =>
207
- Effect.gen(function* () {
208
- const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
209
- useGlobalReactivityGraph: false,
210
- })
211
- const renderCount = makeRenderCount()
212
-
213
- store.mutate(
214
- todos.insert({ id: 't1', text: 'buy milk', completed: false }),
215
- todos.insert({ id: 't2', text: 'buy bread', completed: false }),
216
- )
217
-
218
- const { result, unmount, rerender } = renderHook(
219
- (userId: string) => {
220
- renderCount.inc()
221
-
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,
233
- )
234
-
235
- return { todos }
236
- },
237
- { wrapper, initialProps: 'u1' },
238
- )
239
-
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
- )
247
-
248
- expect(result.current.todos.length).toBe(2)
249
- // expect(result.current.state.username).toBe('')
250
- expect(renderCount.val).toBe(1)
251
-
252
- rerender('u2')
253
-
254
- expect(result.current.todos.length).toBe(1)
255
- expect(renderCount.val).toBe(2)
256
-
257
- unmount()
258
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
259
-
260
- let cachedProvider: BasicTracerProvider | undefined
261
-
262
- describe('otel', () => {
263
- const exporter = new InMemorySpanExporter()
264
-
265
- const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
266
- cachedProvider = provider
267
- provider.register()
268
-
269
- const otelTracer = otel.trace.getTracer('test')
270
-
271
- const span = otelTracer.startSpan('test-root')
272
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
273
-
274
- it('should update the data based on component key', async () => {
275
- const { strictMode } = await Effect.gen(function* () {
276
- const { wrapper, store, reactivityGraph, makeRenderCount, strictMode } = yield* makeTodoMvcReact({
277
- useGlobalReactivityGraph: false,
278
- otelContext,
279
- otelTracer,
280
- })
281
-
282
- const renderCount = makeRenderCount()
283
-
284
- const { result, rerender, unmount } = renderHook(
285
- (userId: string) => {
286
- renderCount.inc()
287
-
288
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
289
- return { state, setState }
290
- },
291
- { wrapper, initialProps: 'u1' },
292
- )
293
-
294
- expect(result.current.state.id).toBe('u1')
295
- expect(result.current.state.username).toBe('')
296
- expect(renderCount.val).toBe(1)
297
-
298
- React.act(() =>
299
- store.mutate(
300
- LiveStore.rawSqlMutation({
301
- sql: LiveStore.sql`INSERT INTO UserInfo (id, username) VALUES ('u2', 'username_u2')`,
302
- }),
303
- ),
304
- )
305
-
306
- rerender('u2')
307
-
308
- expect(result.current.state.id).toBe('u2')
309
- expect(result.current.state.username).toBe('username_u2')
310
- expect(renderCount.val).toBe(2)
311
-
312
- unmount()
313
- span.end()
314
-
315
- return { strictMode }
316
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
317
-
318
- const mapAttributes = (attributes: otel.Attributes) => {
319
- return ReadonlyRecord.map(attributes, (val, key) => {
320
- if (key === 'stackInfo') {
321
- const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
322
- // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
323
- stackInfo.frames.forEach((_) => {
324
- if (_.name.includes('renderHook.wrapper')) {
325
- _.name = 'renderHook.wrapper'
326
- }
327
- _.filePath = '__REPLACED_FOR_SNAPSHOT__'
328
- })
329
- return JSON.stringify(stackInfo)
330
- }
331
- return val
332
- })
333
- }
334
-
335
- // TODO improve testing setup so "obsolete" warning is avoided
336
- if (strictMode) {
337
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=true')
338
- } else {
339
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot('strictMode=false')
340
- }
341
- })
342
- })
343
- })
package/src/useRow.ts DELETED
@@ -1,188 +0,0 @@
1
- import type { QueryInfo, RowQuery } from '@livestore/common'
2
- import { SessionIdSymbol } from '@livestore/common'
3
- import { DbSchema } from '@livestore/common/schema'
4
- import type { SqliteDsl } from '@livestore/db-schema'
5
- import type { LiveQuery, ReactivityGraph } from '@livestore/livestore'
6
- import { queryDb } from '@livestore/livestore'
7
- import { shouldNeverHappen } from '@livestore/utils'
8
- import { ReadonlyRecord } from '@livestore/utils/effect'
9
- import React from 'react'
10
-
11
- import { useStore } from './LiveStoreContext.js'
12
- import { useQueryRef } from './useQuery.js'
13
- import { useMakeScopedQuery } from './useScopedQuery.js'
14
-
15
- export type UseRowResult<TTableDef extends DbSchema.TableDefBase> = [
16
- row: RowQuery.Result<TTableDef>,
17
- setRow: StateSetters<TTableDef>,
18
- query$: LiveQuery<RowQuery.Result<TTableDef>, QueryInfo>,
19
- ]
20
-
21
- export type UseRowOptionsBase = {
22
- reactivityGraph?: ReactivityGraph
23
- }
24
-
25
- /**
26
- * Similar to `React.useState` but returns a tuple of `[row, setRow, query$]` for a given table where ...
27
- *
28
- * - `row` is the current value of the row (fully decoded according to the table schema)
29
- * - `setRow` is a function that can be used to update the row (values will be encoded according to the table schema)
30
- * - `query$` is a `LiveQuery` that e.g. can be used to subscribe to changes to the row
31
- *
32
- * If the table is a singleton table, `useRow` can be called without an `id` argument. Otherwise, the `id` argument is required.
33
- */
34
- export const useRow: {
35
- <
36
- TTableDef extends DbSchema.TableDef<
37
- DbSchema.DefaultSqliteTableDef,
38
- DbSchema.TableOptions & { isSingleton: true; deriveMutations: { enabled: true } }
39
- >,
40
- >(
41
- table: TTableDef,
42
- options?: UseRowOptionsBase,
43
- ): UseRowResult<TTableDef>
44
- <
45
- TTableDef extends DbSchema.TableDef<
46
- DbSchema.DefaultSqliteTableDef,
47
- DbSchema.TableOptions & {
48
- isSingleton: false
49
- requiredInsertColumnNames: 'id'
50
- deriveMutations: { enabled: true }
51
- }
52
- >,
53
- >(
54
- table: TTableDef,
55
- // TODO adjust so it works with arbitrary primary keys or unique constraints
56
- id: string | SessionIdSymbol,
57
- options?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
58
- ): UseRowResult<TTableDef>
59
- <
60
- TTableDef extends DbSchema.TableDef<
61
- DbSchema.DefaultSqliteTableDef,
62
- DbSchema.TableOptions & { isSingleton: false; deriveMutations: { enabled: true } }
63
- >,
64
- >(
65
- table: TTableDef,
66
- // TODO adjust so it works with arbitrary primary keys or unique constraints
67
- id: string | SessionIdSymbol,
68
- options: UseRowOptionsBase & RowQuery.RequiredColumnsOptions<TTableDef>,
69
- ): UseRowResult<TTableDef>
70
- } = <
71
- TTableDef extends DbSchema.TableDef<
72
- DbSchema.DefaultSqliteTableDefConstrained,
73
- DbSchema.TableOptions & { deriveMutations: { enabled: true } }
74
- >,
75
- >(
76
- table: TTableDef,
77
- idOrOptions?: string | SessionIdSymbol | UseRowOptionsBase,
78
- options_?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
79
- ): UseRowResult<TTableDef> => {
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 ?? {}
85
-
86
- type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
87
-
88
- const tableName = table.sqliteDef.name
89
-
90
- if (DbSchema.tableHasDerivedMutations(table) === false) {
91
- shouldNeverHappen(`useRow called on table "${tableName}" which does not have 'deriveMutations: true' set`)
92
- }
93
-
94
- const { store } = useStore()
95
-
96
- if (
97
- store.schema.tables.has(table.sqliteDef.name) === false &&
98
- table.sqliteDef.name.startsWith('__livestore') === false
99
- ) {
100
- shouldNeverHappen(`Table "${table.sqliteDef.name}" not found in schema`)
101
- }
102
-
103
- // console.debug('useRow', tableName, id)
104
-
105
- const idStr = id === SessionIdSymbol ? 'session' : id
106
- const rowQuery = table.query.row as any
107
-
108
- type Query$ = LiveQuery<RowQuery.Result<TTableDef>, QueryInfo.Row>
109
- const { query$, otelContext } = useMakeScopedQuery(
110
- (otelContext) =>
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
- },
121
- )
122
-
123
- const query$Ref = useQueryRef(query$, otelContext) as React.MutableRefObject<RowQuery.Result<TTableDef>>
124
-
125
- const setState = React.useMemo<StateSetters<TTableDef>>(() => {
126
- if (table.options.isSingleColumn) {
127
- return (newValueOrFn: RowQuery.Result<TTableDef>) => {
128
- const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
129
- if (query$Ref.current === newValue) return
130
-
131
- // NOTE we need to account for the short-hand syntax for single-column+singleton tables
132
- if (table.options.isSingleton) {
133
- store.mutate(table.update(newValue))
134
- } else {
135
- store.mutate(table.update({ where: { id }, values: { value: newValue } }))
136
- }
137
- // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { value: newValue }))
138
- }
139
- } else {
140
- const setState = // TODO: do we have a better type for the values that can go in SQLite?
141
- ReadonlyRecord.map(sqliteTableDef.columns, (column, columnName) => (newValueOrFn: any) => {
142
- const newValue =
143
- // @ts-expect-error TODO fix typing
144
- typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current[columnName]) : newValueOrFn
145
-
146
- // Don't update the state if it's the same as the value already seen in the component
147
- // @ts-expect-error TODO fix typing
148
- if (query$Ref.current[columnName] === newValue) return
149
-
150
- store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: { [columnName]: newValue } }))
151
- // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, { [columnName]: newValue }))
152
- })
153
-
154
- setState.setMany = (columnValuesOrFn: Partial<TComponentState>) => {
155
- const columnValues =
156
- // @ts-expect-error TODO fix typing
157
- typeof columnValuesOrFn === 'function' ? columnValuesOrFn(query$Ref.current) : columnValuesOrFn
158
-
159
- // TODO use hashing instead
160
- // Don't update the state if it's the same as the value already seen in the component
161
- if (
162
- // @ts-expect-error TODO fix typing
163
- Object.entries(columnValues).every(([columnName, value]) => query$Ref.current[columnName] === value)
164
- ) {
165
- return
166
- }
167
-
168
- store.mutate(table.update({ where: { id: id ?? 'singleton' }, values: columnValues }))
169
- // store.mutate(updateMutationForQueryInfo(query$.queryInfo!, columnValues))
170
- }
171
-
172
- return setState as any
173
- }
174
- }, [id, query$Ref, sqliteTableDef.columns, store, table])
175
-
176
- return [query$Ref.current, setState, query$]
177
- }
178
-
179
- export type Dispatch<A> = (action: A) => void
180
- export type SetStateAction<S> = S | ((previousValue: S) => S)
181
-
182
- export type StateSetters<TTableDef extends DbSchema.TableDefBase> = TTableDef['options']['isSingleColumn'] extends true
183
- ? Dispatch<SetStateAction<RowQuery.Result<TTableDef>>>
184
- : {
185
- [K in keyof RowQuery.Result<TTableDef>]: Dispatch<SetStateAction<RowQuery.Result<TTableDef>[K]>>
186
- } & {
187
- setMany: Dispatch<SetStateAction<Partial<RowQuery.Result<TTableDef>>>>
188
- }
@@ -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
- })