@livestore/react 0.3.0-dev.9 → 0.3.1-dev.0

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 (97) hide show
  1. package/LICENSE +201 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/LiveStoreContext.d.ts +10 -4
  4. package/dist/LiveStoreContext.d.ts.map +1 -1
  5. package/dist/LiveStoreContext.js +1 -11
  6. package/dist/LiveStoreContext.js.map +1 -1
  7. package/dist/LiveStoreProvider.d.ts +29 -12
  8. package/dist/LiveStoreProvider.d.ts.map +1 -1
  9. package/dist/LiveStoreProvider.js +84 -55
  10. package/dist/LiveStoreProvider.js.map +1 -1
  11. package/dist/LiveStoreProvider.test.js +80 -29
  12. package/dist/LiveStoreProvider.test.js.map +1 -1
  13. package/dist/__tests__/fixture.d.ts +122 -556
  14. package/dist/__tests__/fixture.d.ts.map +1 -1
  15. package/dist/__tests__/fixture.js +71 -30
  16. package/dist/__tests__/fixture.js.map +1 -1
  17. package/dist/experimental/components/LiveList.d.ts +2 -2
  18. package/dist/experimental/components/LiveList.d.ts.map +1 -1
  19. package/dist/experimental/components/LiveList.js +10 -6
  20. package/dist/experimental/components/LiveList.js.map +1 -1
  21. package/dist/mod.d.ts +4 -5
  22. package/dist/mod.d.ts.map +1 -1
  23. package/dist/mod.js +4 -5
  24. package/dist/mod.js.map +1 -1
  25. package/dist/useClientDocument.d.ts +61 -0
  26. package/dist/useClientDocument.d.ts.map +1 -0
  27. package/dist/useClientDocument.js +79 -0
  28. package/dist/useClientDocument.js.map +1 -0
  29. package/dist/useClientDocument.test.d.ts +2 -0
  30. package/dist/useClientDocument.test.d.ts.map +1 -0
  31. package/dist/useClientDocument.test.js +175 -0
  32. package/dist/useClientDocument.test.js.map +1 -0
  33. package/dist/useQuery.d.ts +25 -3
  34. package/dist/useQuery.d.ts.map +1 -1
  35. package/dist/useQuery.js +67 -47
  36. package/dist/useQuery.js.map +1 -1
  37. package/dist/useQuery.test.d.ts +1 -1
  38. package/dist/useQuery.test.d.ts.map +1 -1
  39. package/dist/useQuery.test.js +86 -24
  40. package/dist/useQuery.test.js.map +1 -1
  41. package/dist/useRcResource.d.ts +76 -0
  42. package/dist/useRcResource.d.ts.map +1 -0
  43. package/dist/useRcResource.js +152 -0
  44. package/dist/useRcResource.js.map +1 -0
  45. package/dist/useRcResource.test.d.ts +2 -0
  46. package/dist/useRcResource.test.d.ts.map +1 -0
  47. package/dist/useRcResource.test.js +122 -0
  48. package/dist/useRcResource.test.js.map +1 -0
  49. package/dist/useStore.d.ts +9 -0
  50. package/dist/useStore.d.ts.map +1 -0
  51. package/dist/useStore.js +28 -0
  52. package/dist/useStore.js.map +1 -0
  53. package/dist/utils/useStateRefWithReactiveInput.d.ts.map +1 -1
  54. package/package.json +20 -13
  55. package/src/LiveStoreContext.ts +11 -16
  56. package/src/LiveStoreProvider.test.tsx +176 -37
  57. package/src/LiveStoreProvider.tsx +156 -81
  58. package/src/__snapshots__/useClientDocument.test.tsx.snap +613 -0
  59. package/src/__snapshots__/useQuery.test.tsx.snap +2011 -0
  60. package/src/__tests__/fixture.tsx +74 -47
  61. package/src/experimental/components/LiveList.tsx +10 -7
  62. package/src/mod.ts +5 -6
  63. package/src/useClientDocument.test.tsx +306 -0
  64. package/src/useClientDocument.ts +157 -0
  65. package/src/useQuery.test.tsx +182 -71
  66. package/src/useQuery.ts +95 -58
  67. package/src/useRcResource.test.tsx +167 -0
  68. package/src/useRcResource.ts +182 -0
  69. package/src/useStore.ts +36 -0
  70. package/dist/useAtom.d.ts +0 -5
  71. package/dist/useAtom.d.ts.map +0 -1
  72. package/dist/useAtom.js +0 -38
  73. package/dist/useAtom.js.map +0 -1
  74. package/dist/useRow.d.ts +0 -50
  75. package/dist/useRow.d.ts.map +0 -1
  76. package/dist/useRow.js +0 -93
  77. package/dist/useRow.js.map +0 -1
  78. package/dist/useRow.test.d.ts +0 -2
  79. package/dist/useRow.test.d.ts.map +0 -1
  80. package/dist/useRow.test.js +0 -202
  81. package/dist/useRow.test.js.map +0 -1
  82. package/dist/useScopedQuery.d.ts +0 -33
  83. package/dist/useScopedQuery.d.ts.map +0 -1
  84. package/dist/useScopedQuery.js +0 -87
  85. package/dist/useScopedQuery.js.map +0 -1
  86. package/dist/useScopedQuery.test.d.ts +0 -2
  87. package/dist/useScopedQuery.test.d.ts.map +0 -1
  88. package/dist/useScopedQuery.test.js +0 -60
  89. package/dist/useScopedQuery.test.js.map +0 -1
  90. package/src/__snapshots__/useRow.test.tsx.snap +0 -360
  91. package/src/useAtom.ts +0 -52
  92. package/src/useRow.test.tsx +0 -344
  93. package/src/useRow.ts +0 -188
  94. package/src/useScopedQuery.test.tsx +0 -96
  95. package/src/useScopedQuery.ts +0 -143
  96. package/tsconfig.json +0 -20
  97. package/vitest.config.js +0 -17
@@ -1,344 +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
- // const strictMode = process.env.REACT_STRICT_MODE !== undefined
14
-
15
- // 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
- })
22
-
23
- const renderCount = makeRenderCount()
24
-
25
- const { result, rerender } = renderHook(
26
- (userId: string) => {
27
- renderCount.inc()
28
-
29
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
30
- return { state, setState }
31
- },
32
- { wrapper, initialProps: 'u1' },
33
- )
34
-
35
- expect(result.current.state.id).toBe('u1')
36
- expect(result.current.state.username).toBe('')
37
- 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
- )
46
-
47
- rerender('u2')
48
-
49
- expect(result.current.state.id).toBe('u2')
50
- expect(result.current.state.username).toBe('username_u2')
51
- expect(renderCount.val).toBe(2)
52
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
53
-
54
- // TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
55
-
56
- it('should update the data reactively - via setState', () =>
57
- Effect.gen(function* () {
58
- const { wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
59
- useGlobalReactivityGraph: false,
60
- })
61
-
62
- const renderCount = makeRenderCount()
63
-
64
- const { result } = renderHook(
65
- (userId: string) => {
66
- renderCount.inc()
67
-
68
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
69
- return { state, setState }
70
- },
71
- { wrapper, initialProps: 'u1' },
72
- )
73
-
74
- expect(result.current.state.id).toBe('u1')
75
- expect(result.current.state.username).toBe('')
76
- expect(renderCount.val).toBe(1)
77
-
78
- React.act(() => result.current.setState.username('username_u1_hello'))
79
-
80
- expect(result.current.state.id).toBe('u1')
81
- expect(result.current.state.username).toBe('username_u1_hello')
82
- expect(renderCount.val).toBe(2)
83
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
84
-
85
- it('should update the data reactively - via raw store mutation', () =>
86
- Effect.gen(function* () {
87
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
88
- useGlobalReactivityGraph: false,
89
- })
90
-
91
- const renderCount = makeRenderCount()
92
-
93
- const { result } = renderHook(
94
- (userId: string) => {
95
- renderCount.inc()
96
-
97
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
98
- return { state, setState }
99
- },
100
- { wrapper, initialProps: 'u1' },
101
- )
102
-
103
- expect(result.current.state.id).toBe('u1')
104
- expect(result.current.state.username).toBe('')
105
- expect(renderCount.val).toBe(1)
106
-
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
- ),
113
- )
114
-
115
- expect(result.current.state.id).toBe('u1')
116
- expect(result.current.state.username).toBe('username_u1_hello')
117
- expect(renderCount.val).toBe(2)
118
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
119
-
120
- it('should work for a larger app', () =>
121
- Effect.gen(function* () {
122
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
123
- useGlobalReactivityGraph: false,
124
- })
125
-
126
- const allTodos$ = LiveStore.queryDb(
127
- { query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
128
- { label: 'allTodos', reactivityGraph },
129
- )
130
-
131
- const appRouterRenderCount = makeRenderCount()
132
- let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
133
- const AppRouter: React.FC = () => {
134
- appRouterRenderCount.inc()
135
-
136
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema, { reactivityGraph })
137
-
138
- globalSetState = setState
139
-
140
- return (
141
- <div>
142
- <TasksList setTaskId={setState.currentTaskId} />
143
- <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
144
- {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
145
- </div>
146
- )
147
- }
148
-
149
- const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
150
- const allTodos = LiveStoreReact.useQuery(allTodos$)
151
-
152
- return (
153
- <div>
154
- {allTodos.map((_) => (
155
- <div key={_.id} onClick={() => setTaskId(_.id)}>
156
- {_.id}
157
- </div>
158
- ))}
159
- </div>
160
- )
161
- }
162
-
163
- const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
164
- const [todo] = LiveStoreReact.useRow(todos, id, { reactivityGraph })
165
- return <div role="content">{JSON.stringify(todo)}</div>
166
- }
167
-
168
- const renderResult = render(<AppRouter />, { wrapper })
169
-
170
- expect(appRouterRenderCount.val).toBe(1)
171
-
172
- React.act(() =>
173
- store.mutate(
174
- LiveStore.rawSqlMutation({
175
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
176
- }),
177
- ),
178
- )
179
-
180
- expect(appRouterRenderCount.val).toBe(1)
181
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
182
-
183
- React.act(() => globalSetState!.currentTaskId('t1'))
184
-
185
- expect(appRouterRenderCount.val).toBe(2)
186
- expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
187
- `"{"id":"t1","text":"buy milk","completed":false}"`,
188
- )
189
-
190
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t1"')
191
-
192
- React.act(() =>
193
- store.mutate(
194
- LiveStore.rawSqlMutation({
195
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
196
- }),
197
- AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
198
- LiveStore.rawSqlMutation({
199
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
200
- }),
201
- ),
202
- )
203
-
204
- expect(appRouterRenderCount.val).toBe(3)
205
- expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: t2"')
206
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
207
-
208
- it('should work for a useRow query chained with a useTemporary query', () =>
209
- Effect.gen(function* () {
210
- const { store, wrapper, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
211
- useGlobalReactivityGraph: false,
212
- })
213
- const renderCount = makeRenderCount()
214
-
215
- store.mutate(
216
- todos.insert({ id: 't1', text: 'buy milk', completed: false }),
217
- todos.insert({ id: 't2', text: 'buy bread', completed: false }),
218
- )
219
-
220
- const { result, unmount, rerender } = renderHook(
221
- (userId: string) => {
222
- renderCount.inc()
223
-
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,
235
- )
236
-
237
- return { todos }
238
- },
239
- { wrapper, initialProps: 'u1' },
240
- )
241
-
242
- React.act(() =>
243
- store.mutate(
244
- LiveStore.rawSqlMutation({
245
- sql: LiveStore.sql`INSERT INTO UserInfo (id, username, text) VALUES ('u2', 'username_u2', 'milk')`,
246
- }),
247
- ),
248
- )
249
-
250
- expect(result.current.todos.length).toBe(2)
251
- // expect(result.current.state.username).toBe('')
252
- expect(renderCount.val).toBe(1)
253
-
254
- rerender('u2')
255
-
256
- expect(result.current.todos.length).toBe(1)
257
- expect(renderCount.val).toBe(2)
258
-
259
- unmount()
260
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise))
261
-
262
- describe('otel', () => {
263
- const provider = new BasicTracerProvider({})
264
- provider.register()
265
-
266
- it.each([{ strictMode: true }, { strictMode: false }])(
267
- 'should update the data based on component key strictMode=%s',
268
- async ({ strictMode }) => {
269
- const exporter = new InMemorySpanExporter()
270
-
271
- // const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
272
- provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
273
-
274
- const otelTracer = otel.trace.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
275
-
276
- const span = otelTracer.startSpan('test-root')
277
- const otelContext = otel.trace.setSpan(otel.context.active(), span)
278
-
279
- await Effect.gen(function* () {
280
- const { wrapper, store, reactivityGraph, makeRenderCount } = yield* makeTodoMvcReact({
281
- useGlobalReactivityGraph: false,
282
- otelContext,
283
- otelTracer,
284
- strictMode,
285
- })
286
-
287
- const renderCount = makeRenderCount()
288
-
289
- const { result, rerender, unmount } = renderHook(
290
- (userId: string) => {
291
- renderCount.inc()
292
-
293
- const [state, setState] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
294
- return { state, setState }
295
- },
296
- { wrapper, initialProps: 'u1' },
297
- )
298
-
299
- expect(result.current.state.id).toBe('u1')
300
- expect(result.current.state.username).toBe('')
301
- expect(renderCount.val).toBe(1)
302
-
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
- )
310
-
311
- rerender('u2')
312
-
313
- expect(result.current.state.id).toBe('u2')
314
- expect(result.current.state.username).toBe('username_u2')
315
- expect(renderCount.val).toBe(2)
316
-
317
- unmount()
318
- span.end()
319
-
320
- return { strictMode }
321
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
322
-
323
- const mapAttributes = (attributes: otel.Attributes) => {
324
- return ReadonlyRecord.map(attributes, (val, key) => {
325
- if (key === 'stackInfo') {
326
- const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
327
- // stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
328
- stackInfo.frames.forEach((_) => {
329
- if (_.name.includes('renderHook.wrapper')) {
330
- _.name = 'renderHook.wrapper'
331
- }
332
- _.filePath = '__REPLACED_FOR_SNAPSHOT__'
333
- })
334
- return JSON.stringify(stackInfo)
335
- }
336
- return val
337
- })
338
- }
339
-
340
- expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
341
- },
342
- )
343
- })
344
- })
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.RefObject<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
- })