@livestore/react 0.3.0-dev.28 → 0.3.0-dev.29

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 (64) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/LiveStoreContext.d.ts +10 -6
  3. package/dist/LiveStoreContext.d.ts.map +1 -1
  4. package/dist/LiveStoreContext.js +0 -14
  5. package/dist/LiveStoreContext.js.map +1 -1
  6. package/dist/LiveStoreProvider.d.ts +2 -2
  7. package/dist/LiveStoreProvider.d.ts.map +1 -1
  8. package/dist/LiveStoreProvider.js +5 -1
  9. package/dist/LiveStoreProvider.js.map +1 -1
  10. package/dist/LiveStoreProvider.test.js +6 -5
  11. package/dist/LiveStoreProvider.test.js.map +1 -1
  12. package/dist/__tests__/fixture.d.ts +115 -546
  13. package/dist/__tests__/fixture.d.ts.map +1 -1
  14. package/dist/__tests__/fixture.js +64 -22
  15. package/dist/__tests__/fixture.js.map +1 -1
  16. package/dist/mod.d.ts +4 -4
  17. package/dist/mod.d.ts.map +1 -1
  18. package/dist/mod.js +4 -4
  19. package/dist/mod.js.map +1 -1
  20. package/dist/useClientDocument.d.ts +61 -0
  21. package/dist/useClientDocument.d.ts.map +1 -0
  22. package/dist/useClientDocument.js +79 -0
  23. package/dist/useClientDocument.js.map +1 -0
  24. package/dist/useClientDocument.test.d.ts +2 -0
  25. package/dist/useClientDocument.test.d.ts.map +1 -0
  26. package/dist/{useRow.test.js → useClientDocument.test.js} +44 -48
  27. package/dist/useClientDocument.test.js.map +1 -0
  28. package/dist/useQuery.d.ts +1 -3
  29. package/dist/useQuery.d.ts.map +1 -1
  30. package/dist/useQuery.js +6 -3
  31. package/dist/useQuery.js.map +1 -1
  32. package/dist/useQuery.test.js +16 -17
  33. package/dist/useQuery.test.js.map +1 -1
  34. package/dist/useStore.d.ts +9 -0
  35. package/dist/useStore.d.ts.map +1 -0
  36. package/dist/useStore.js +28 -0
  37. package/dist/useStore.js.map +1 -0
  38. package/package.json +11 -11
  39. package/src/LiveStoreContext.ts +10 -19
  40. package/src/LiveStoreProvider.test.tsx +6 -5
  41. package/src/LiveStoreProvider.tsx +7 -4
  42. package/src/__snapshots__/{useRow.test.tsx.snap → useClientDocument.test.tsx.snap} +62 -46
  43. package/src/__snapshots__/useQuery.test.tsx.snap +8 -8
  44. package/src/__tests__/fixture.tsx +69 -39
  45. package/src/mod.ts +5 -5
  46. package/src/{useRow.test.tsx → useClientDocument.test.tsx} +51 -55
  47. package/src/useClientDocument.ts +157 -0
  48. package/src/useQuery.test.tsx +18 -19
  49. package/src/useQuery.ts +9 -8
  50. package/src/useStore.ts +36 -0
  51. package/tmp/pack.tgz +0 -0
  52. package/dist/useAtom.d.ts +0 -8
  53. package/dist/useAtom.d.ts.map +0 -1
  54. package/dist/useAtom.js +0 -42
  55. package/dist/useAtom.js.map +0 -1
  56. package/dist/useRow.d.ts +0 -64
  57. package/dist/useRow.d.ts.map +0 -1
  58. package/dist/useRow.js +0 -108
  59. package/dist/useRow.js.map +0 -1
  60. package/dist/useRow.test.d.ts +0 -2
  61. package/dist/useRow.test.d.ts.map +0 -1
  62. package/dist/useRow.test.js.map +0 -1
  63. package/src/useAtom.ts +0 -66
  64. package/src/useRow.ts +0 -210
@@ -8,14 +8,14 @@ import * as ReactTesting from '@testing-library/react'
8
8
  import React from 'react'
9
9
  import { beforeEach, expect, it } from 'vitest'
10
10
 
11
- import { AppRouterSchema, makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
12
- import * as LiveStoreReact from './mod.js'
11
+ import { events, makeTodoMvcReact, tables } from './__tests__/fixture.js'
12
+ import type * as LiveStoreReact from './mod.js'
13
13
  import { __resetUseRcResourceCache } from './useRcResource.js'
14
14
 
15
15
  // const strictMode = process.env.REACT_STRICT_MODE !== undefined
16
16
 
17
17
  // NOTE running tests concurrently doesn't work with the default global db graph
18
- Vitest.describe('useRow', () => {
18
+ Vitest.describe('useClientDocument', () => {
19
19
  beforeEach(() => {
20
20
  __resetUseRcResourceCache()
21
21
  })
@@ -28,22 +28,22 @@ Vitest.describe('useRow', () => {
28
28
  (userId: string) => {
29
29
  renderCount.inc()
30
30
 
31
- const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
32
- return { state, setState }
31
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
32
+ return { state, setState, id }
33
33
  },
34
34
  { wrapper, initialProps: 'u1' },
35
35
  )
36
36
 
37
- expect(result.current.state.id).toBe('u1')
37
+ expect(result.current.id).toBe('u1')
38
38
  expect(result.current.state.username).toBe('')
39
39
  expect(renderCount.val).toBe(1)
40
40
  expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
41
- store.commit(tables.userInfo.insert({ id: 'u2', username: 'username_u2' }))
41
+ store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
42
42
 
43
43
  rerender('u2')
44
44
 
45
45
  expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
46
- expect(result.current.state.id).toBe('u2')
46
+ expect(result.current.id).toBe('u2')
47
47
  expect(result.current.state.username).toBe('username_u2')
48
48
  expect(renderCount.val).toBe(2)
49
49
  }),
@@ -53,31 +53,31 @@ Vitest.describe('useRow', () => {
53
53
 
54
54
  Vitest.scopedLive('should update the data reactively - via setState', () =>
55
55
  Effect.gen(function* () {
56
- const { wrapper, renderCount } = yield* makeTodoMvcReact({})
56
+ const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
57
57
 
58
58
  const { result } = ReactTesting.renderHook(
59
59
  (userId: string) => {
60
60
  renderCount.inc()
61
61
 
62
- const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
63
- return { state, setState }
62
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
63
+ return { state, setState, id }
64
64
  },
65
65
  { wrapper, initialProps: 'u1' },
66
66
  )
67
67
 
68
- expect(result.current.state.id).toBe('u1')
68
+ expect(result.current.id).toBe('u1')
69
69
  expect(result.current.state.username).toBe('')
70
70
  expect(renderCount.val).toBe(1)
71
71
 
72
- ReactTesting.act(() => result.current.setState.username('username_u1_hello'))
72
+ ReactTesting.act(() => result.current.setState({ username: 'username_u1_hello' }))
73
73
 
74
- expect(result.current.state.id).toBe('u1')
74
+ expect(result.current.id).toBe('u1')
75
75
  expect(result.current.state.username).toBe('username_u1_hello')
76
76
  expect(renderCount.val).toBe(2)
77
77
  }),
78
78
  )
79
79
 
80
- Vitest.scopedLive('should update the data reactively - via raw store mutation', () =>
80
+ Vitest.scopedLive('should update the data reactively - via raw store commit', () =>
81
81
  Effect.gen(function* () {
82
82
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
83
83
 
@@ -85,21 +85,19 @@ Vitest.describe('useRow', () => {
85
85
  (userId: string) => {
86
86
  renderCount.inc()
87
87
 
88
- const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
89
- return { state, setState }
88
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
89
+ return { state, setState, id }
90
90
  },
91
91
  { wrapper, initialProps: 'u1' },
92
92
  )
93
93
 
94
- expect(result.current.state.id).toBe('u1')
94
+ expect(result.current.id).toBe('u1')
95
95
  expect(result.current.state.username).toBe('')
96
96
  expect(renderCount.val).toBe(1)
97
97
 
98
- ReactTesting.act(() =>
99
- store.commit(tables.userInfo.update({ where: { id: 'u1' }, values: { username: 'username_u1_hello' } })),
100
- )
98
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u1_hello' }, 'u1')))
101
99
 
102
- expect(result.current.state.id).toBe('u1')
100
+ expect(result.current.id).toBe('u1')
103
101
  expect(result.current.state.username).toBe('username_u1_hello')
104
102
  expect(renderCount.val).toBe(2)
105
103
  }),
@@ -110,21 +108,21 @@ Vitest.describe('useRow', () => {
110
108
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
111
109
 
112
110
  const allTodos$ = LiveStore.queryDb(
113
- { query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
111
+ { query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) },
114
112
  { label: 'allTodos' },
115
113
  )
116
114
 
117
- let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
115
+ let globalSetState: LiveStoreReact.StateSetters<typeof tables.AppRouterSchema> | undefined
118
116
  const AppRouter: React.FC = () => {
119
117
  renderCount.inc()
120
118
 
121
- const [state, setState] = LiveStoreReact.useRow(AppRouterSchema)
119
+ const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
122
120
 
123
121
  globalSetState = setState
124
122
 
125
123
  return (
126
124
  <div>
127
- <TasksList setTaskId={setState.currentTaskId} />
125
+ <TasksList setTaskId={(taskId) => setState({ currentTaskId: taskId })} />
128
126
  <div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
129
127
  {state.currentTaskId ? <TaskDetails id={state.currentTaskId} /> : <div>Click on a task to see details</div>}
130
128
  </div>
@@ -132,7 +130,7 @@ Vitest.describe('useRow', () => {
132
130
  }
133
131
 
134
132
  const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
135
- const allTodos = LiveStoreReact.useQuery(allTodos$)
133
+ const allTodos = store.useQuery(allTodos$)
136
134
 
137
135
  return (
138
136
  <div>
@@ -146,7 +144,7 @@ Vitest.describe('useRow', () => {
146
144
  }
147
145
 
148
146
  const TaskDetails: React.FC<{ id: string }> = ({ id }) => {
149
- const [todo] = LiveStoreReact.useRow(todos, id)
147
+ const todo = store.useQuery(LiveStore.queryDb(tables.todos.where({ id }).first(), { deps: id }))
150
148
  return <div role="content">{JSON.stringify(todo)}</div>
151
149
  }
152
150
 
@@ -156,7 +154,7 @@ Vitest.describe('useRow', () => {
156
154
 
157
155
  ReactTesting.act(() =>
158
156
  store.commit(
159
- LiveStore.rawSqlMutation({
157
+ LiveStore.rawSqlEvent({
160
158
  sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)`,
161
159
  }),
162
160
  ),
@@ -165,7 +163,7 @@ Vitest.describe('useRow', () => {
165
163
  expect(renderCount.val).toBe(1)
166
164
  expect(renderResult.getByRole('current-id').innerHTML).toMatchInlineSnapshot('"Current Task Id: -"')
167
165
 
168
- ReactTesting.act(() => globalSetState!.currentTaskId('t1'))
166
+ ReactTesting.act(() => globalSetState!({ currentTaskId: 't1' }))
169
167
 
170
168
  expect(renderCount.val).toBe(2)
171
169
  expect(renderResult.getByRole('content').innerHTML).toMatchInlineSnapshot(
@@ -176,13 +174,9 @@ Vitest.describe('useRow', () => {
176
174
 
177
175
  ReactTesting.act(() =>
178
176
  store.commit(
179
- LiveStore.rawSqlMutation({
180
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t2', 'buy eggs', 0)`,
181
- }),
182
- AppRouterSchema.update({ where: { id: 'singleton' }, values: { currentTaskId: 't2' } }),
183
- LiveStore.rawSqlMutation({
184
- sql: LiveStore.sql`INSERT INTO todos (id, text, completed) VALUES ('t3', 'buy bread', 0)`,
185
- }),
177
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
178
+ events.AppRouterSet({ currentTaskId: 't2' }),
179
+ events.todoCreated({ id: 't3', text: 'buy bread', completed: false }),
186
180
  ),
187
181
  )
188
182
 
@@ -191,23 +185,23 @@ Vitest.describe('useRow', () => {
191
185
  }),
192
186
  )
193
187
 
194
- Vitest.scopedLive('should work for a useRow query chained with a useTemporary query', () =>
188
+ Vitest.scopedLive('should work for a useClientDocument query chained with a useTemporary query', () =>
195
189
  Effect.gen(function* () {
196
190
  const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
197
191
 
198
192
  store.commit(
199
- todos.insert({ id: 't1', text: 'buy milk', completed: false }),
200
- todos.insert({ id: 't2', text: 'buy bread', completed: false }),
193
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
194
+ events.todoCreated({ id: 't2', text: 'buy bread', completed: false }),
201
195
  )
202
196
 
203
197
  const { result, unmount, rerender } = ReactTesting.renderHook(
204
198
  (userId: string) => {
205
199
  renderCount.inc()
206
200
 
207
- const [_row, _setRow, rowState$] = LiveStoreReact.useRow(tables.userInfo, userId)
208
- const todos = LiveStoreReact.useQuery(
201
+ const [_row, _setRow, _id, rowState$] = store.useClientDocument(tables.userInfo, userId)
202
+ const todos = store.useQuery(
209
203
  LiveStore.queryDb(
210
- (get) => tables.todos.query.where('text', 'LIKE', `%${get(rowState$).text}%`),
204
+ (get) => tables.todos.where('text', 'LIKE', `%${get(rowState$).text}%`),
211
205
  // TODO find a way where explicit `userId` is not needed here
212
206
  // possibly by automatically understanding the `get(rowState$)` dependency
213
207
  { label: 'todosFiltered', deps: userId },
@@ -220,7 +214,7 @@ Vitest.describe('useRow', () => {
220
214
  { wrapper, initialProps: 'u1' },
221
215
  )
222
216
 
223
- ReactTesting.act(() => store.commit(tables.userInfo.insert({ id: 'u2', username: 'username_u2', text: 'milk' })))
217
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2', text: 'milk' }, 'u2')))
224
218
 
225
219
  expect(result.current.todos.length).toBe(2)
226
220
  expect(renderCount.val).toBe(1)
@@ -235,18 +229,16 @@ Vitest.describe('useRow', () => {
235
229
  )
236
230
 
237
231
  Vitest.describe('otel', () => {
238
- const provider = new BasicTracerProvider({})
239
- provider.register()
240
-
241
232
  it.each([{ strictMode: true }, { strictMode: false }])(
242
233
  'should update the data based on component key strictMode=%s',
243
234
  async ({ strictMode }) => {
244
235
  const exporter = new InMemorySpanExporter()
245
236
 
246
- // const provider = cachedProvider ?? new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
247
- provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
237
+ const provider = new BasicTracerProvider({
238
+ spanProcessors: [new SimpleSpanProcessor(exporter)],
239
+ })
248
240
 
249
- const otelTracer = otel.trace.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
241
+ const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
250
242
 
251
243
  const span = otelTracer.startSpan('test-root')
252
244
  const otelContext = otel.trace.setSpan(otel.context.active(), span)
@@ -262,23 +254,23 @@ Vitest.describe('useRow', () => {
262
254
  (userId: string) => {
263
255
  renderCount.inc()
264
256
 
265
- const [state, setState] = LiveStoreReact.useRow(tables.userInfo, userId)
266
- return { state, setState }
257
+ const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
258
+ return { state, setState, id }
267
259
  },
268
260
  { wrapper, initialProps: 'u1' },
269
261
  )
270
262
 
271
- expect(result.current.state.id).toBe('u1')
263
+ expect(result.current.id).toBe('u1')
272
264
  expect(result.current.state.username).toBe('')
273
265
  expect(renderCount.val).toBe(1)
274
266
 
275
267
  // For u2 we'll make sure that the row already exists,
276
268
  // so the lazy `insert` will be skipped
277
- ReactTesting.act(() => store.commit(tables.userInfo.insert({ id: 'u2', username: 'username_u2' })))
269
+ ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
278
270
 
279
271
  rerender('u2')
280
272
 
281
- expect(result.current.state.id).toBe('u2')
273
+ expect(result.current.id).toBe('u2')
282
274
  expect(result.current.state.username).toBe('username_u2')
283
275
  expect(renderCount.val).toBe(2)
284
276
 
@@ -286,6 +278,8 @@ Vitest.describe('useRow', () => {
286
278
  span.end()
287
279
  }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
288
280
 
281
+ await provider.forceFlush()
282
+
289
283
  const mapAttributes = (attributes: otel.Attributes) => {
290
284
  return ReadonlyRecord.map(attributes, (val, key) => {
291
285
  if (key === 'firstStackInfo') {
@@ -304,6 +298,8 @@ Vitest.describe('useRow', () => {
304
298
  }
305
299
 
306
300
  expect(getSimplifiedRootSpan(exporter, mapAttributes)).toMatchSnapshot()
301
+
302
+ await provider.shutdown()
307
303
  },
308
304
  )
309
305
  })
@@ -0,0 +1,157 @@
1
+ import type { RowQuery } from '@livestore/common'
2
+ import { SessionIdSymbol } from '@livestore/common'
3
+ import { State } from '@livestore/common/schema'
4
+ import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
5
+ import { queryDb } from '@livestore/livestore'
6
+ import { shouldNeverHappen } from '@livestore/utils'
7
+ import React from 'react'
8
+
9
+ import { LiveStoreContext } from './LiveStoreContext.js'
10
+ import { useQueryRef } from './useQuery.js'
11
+
12
+ export type UseRowResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
13
+ row: TTableDef['Value'],
14
+ setRow: StateSetters<TTableDef>,
15
+ id: string,
16
+ query$: LiveQuery<TTableDef['Value']>,
17
+ ]
18
+
19
+ /**
20
+ * Similar to `React.useState` but returns a tuple of `[state, setState, id, query$]` for a given table where ...
21
+ *
22
+ * - `state` is the current value of the row (fully decoded according to the table schema)
23
+ * - `setState` is a function that can be used to update the document
24
+ * - `id` is the id of the document
25
+ * - `query$` is a `LiveQuery` that e.g. can be used to subscribe to changes to the document
26
+ *
27
+ * `useClientDocument` only works for client-document tables:
28
+ *
29
+ * ```tsx
30
+ * const MyState = State.SQLite.clientDocument({
31
+ * name: 'MyState',
32
+ * schema: Schema.Struct({
33
+ * showSidebar: Schema.Boolean,
34
+ * }),
35
+ * default: { id: SessionIdSymbol, value: { showSidebar: true } },
36
+ * })
37
+ *
38
+ * const MyComponent = () => {
39
+ * const [{ showSidebar }, setState] = useClientDocument(MyState)
40
+ * return (
41
+ * <div onClick={() => setState({ showSidebar: !showSidebar })}>
42
+ * {showSidebar ? 'Sidebar is open' : 'Sidebar is closed'}
43
+ * </div>
44
+ * )
45
+ * }
46
+ * ```
47
+ *
48
+ * If the table has a default id, `useClientDocument` can be called without an `id` argument. Otherwise, the `id` argument is required.
49
+ */
50
+ export const useClientDocument: {
51
+ // case: with default id
52
+ <
53
+ TTableDef extends State.SQLite.ClientDocumentTableDef.Trait<
54
+ any,
55
+ any,
56
+ any,
57
+ { partialSet: boolean; default: { id: string | SessionIdSymbol; value: any } }
58
+ >,
59
+ >(
60
+ table: TTableDef,
61
+ id?: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | SessionIdSymbol,
62
+ options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
63
+ ): UseRowResult<TTableDef>
64
+
65
+ // case: no default id → id arg is required
66
+ <
67
+ TTableDef extends State.SQLite.ClientDocumentTableDef.Trait<
68
+ any,
69
+ any,
70
+ any,
71
+ { partialSet: boolean; default: { id: string | SessionIdSymbol | undefined; value: any } }
72
+ >,
73
+ >(
74
+ table: TTableDef,
75
+ // TODO adjust so it works with arbitrary primary keys or unique constraints
76
+ id: State.SQLite.ClientDocumentTableDef.DefaultIdType<TTableDef> | string | SessionIdSymbol,
77
+ options?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
78
+ ): UseRowResult<TTableDef>
79
+ } = <TTableDef extends State.SQLite.ClientDocumentTableDef.Any>(
80
+ table: TTableDef,
81
+ idOrOptions?: string | SessionIdSymbol,
82
+ options_?: Partial<RowQuery.GetOrCreateOptions<TTableDef>>,
83
+ storeArg?: { store?: Store },
84
+ ): UseRowResult<TTableDef> => {
85
+ const id =
86
+ typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol
87
+ ? idOrOptions
88
+ : table[State.SQLite.ClientDocumentTableDefSymbol].options.default.id
89
+
90
+ const options: Partial<RowQuery.GetOrCreateOptions<TTableDef>> | undefined =
91
+ typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? options_ : idOrOptions
92
+
93
+ const { default: defaultValues } = options ?? {}
94
+
95
+ React.useMemo(() => validateTableOptions(table), [table])
96
+
97
+ const tableName = table.sqliteDef.name
98
+
99
+ const store =
100
+ storeArg?.store ??
101
+ // eslint-disable-next-line react-hooks/rules-of-hooks
102
+ React.useContext(LiveStoreContext)?.store ??
103
+ shouldNeverHappen(`No store provided to useClientDocument`)
104
+
105
+ // console.debug('useClientDocument', tableName, id)
106
+
107
+ const idStr: string = id === SessionIdSymbol ? store.clientSession.sessionId : id
108
+
109
+ type QueryDef = LiveQueryDef<TTableDef['Value']>
110
+ const queryDef: QueryDef = React.useMemo(
111
+ () =>
112
+ queryDb(table.get(id!, { default: defaultValues! }), {
113
+ deps: [idStr!, table.sqliteDef.name, JSON.stringify(defaultValues)],
114
+ }),
115
+ [table, id, defaultValues, idStr],
116
+ )
117
+
118
+ const queryRef = useQueryRef(queryDef, {
119
+ otelSpanName: `LiveStore:useClientDocument:${tableName}:${idStr}`,
120
+ store: storeArg?.store,
121
+ })
122
+
123
+ const setState = React.useMemo<StateSetters<TTableDef>>(
124
+ () => (newValueOrFn: TTableDef['Value']) => {
125
+ const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
126
+ if (queryRef.valueRef.current === newValue) return
127
+
128
+ store.commit(table.set(removeUndefinedValues(newValue), id as any))
129
+ },
130
+ [id, queryRef.valueRef, store, table],
131
+ )
132
+
133
+ return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
134
+ }
135
+
136
+ export type Dispatch<A> = (action: A) => void
137
+ export type SetStateAction<S> = Partial<S> | ((previousValue: S) => Partial<S>)
138
+
139
+ export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
140
+ SetStateAction<TTableDef['Value']>
141
+ >
142
+
143
+ const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
144
+ if (State.SQLite.tableIsClientDocumentTable(table) === false) {
145
+ return shouldNeverHappen(
146
+ `useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
147
+ )
148
+ }
149
+ }
150
+
151
+ const removeUndefinedValues = (value: any) => {
152
+ if (typeof value === 'object' && value !== null) {
153
+ return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
154
+ }
155
+
156
+ return value
157
+ }
@@ -11,8 +11,7 @@ import React from 'react'
11
11
  import * as ReactWindow from 'react-window'
12
12
  import { expect } from 'vitest'
13
13
 
14
- import { makeTodoMvcReact, tables, todos } from './__tests__/fixture.js'
15
- import * as LiveStoreReact from './mod.js'
14
+ import { events, makeTodoMvcReact, tables } from './__tests__/fixture.js'
16
15
  import { __resetUseRcResourceCache } from './useRcResource.js'
17
16
 
18
17
  Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
@@ -27,13 +26,13 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
27
26
  Effect.gen(function* () {
28
27
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
29
28
 
30
- const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.schema) })
29
+ const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.rowSchema) })
31
30
 
32
31
  const { result } = ReactTesting.renderHook(
33
32
  () => {
34
33
  renderCount.inc()
35
34
 
36
- return LiveStoreReact.useQuery(allTodos$)
35
+ return store.useQuery(allTodos$)
37
36
  },
38
37
  { wrapper },
39
38
  )
@@ -42,7 +41,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
42
41
  expect(renderCount.val).toBe(1)
43
42
  expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
44
43
 
45
- ReactTesting.act(() => store.commit(todos.insert({ id: 't1', text: 'buy milk', completed: false })))
44
+ ReactTesting.act(() => store.commit(events.todoCreated({ id: 't1', text: 'buy milk', completed: false })))
46
45
 
47
46
  expect(result.current.length).toBe(1)
48
47
  expect(result.current[0]!.text).toBe('buy milk')
@@ -56,17 +55,17 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
56
55
  const { wrapper, store, renderCount } = yield* makeTodoMvcReact({ strictMode })
57
56
 
58
57
  const todo1$ = queryDb(
59
- { query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.schema) },
58
+ { query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.rowSchema) },
60
59
  { label: 'libraryTracksView1' },
61
60
  )
62
61
  const todo2$ = queryDb(
63
- { query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.schema) },
62
+ { query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.rowSchema) },
64
63
  { label: 'libraryTracksView2' },
65
64
  )
66
65
 
67
66
  store.commit(
68
- todos.insert({ id: 't1', text: 'buy milk', completed: false }),
69
- todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
67
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
68
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
70
69
  )
71
70
 
72
71
  const { result, rerender } = ReactTesting.renderHook(
@@ -75,7 +74,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
75
74
 
76
75
  const query$ = React.useMemo(() => (todoId === 't1' ? todo1$ : todo2$), [todoId])
77
76
 
78
- return LiveStoreReact.useQuery(query$)[0]!.text
77
+ return store.useQuery(query$)[0]!.text
79
78
  },
80
79
  { wrapper, initialProps: 't1' },
81
80
  )
@@ -84,11 +83,11 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
84
83
  expect(renderCount.val).toBe(1)
85
84
  expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('1: after first render')
86
85
 
87
- ReactTesting.act(() => store.commit(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
86
+ ReactTesting.act(() => store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' })))
88
87
 
89
88
  expect(result.current).toBe('buy soy milk')
90
89
  expect(renderCount.val).toBe(2)
91
- expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('2: after first mutation')
90
+ expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot('2: after first commit')
92
91
 
93
92
  rerender('t2')
94
93
 
@@ -104,18 +103,18 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
104
103
 
105
104
  const filter$ = makeRef('t1', { label: 'id-filter' })
106
105
 
107
- const todo$ = queryDb((get) => tables.todos.query.where('id', get(filter$)), { label: 'todo' })
106
+ const todo$ = queryDb((get) => tables.todos.where('id', get(filter$)), { label: 'todo' })
108
107
 
109
108
  store.commit(
110
- todos.insert({ id: 't1', text: 'buy milk', completed: false }),
111
- todos.insert({ id: 't2', text: 'buy eggs', completed: false }),
109
+ events.todoCreated({ id: 't1', text: 'buy milk', completed: false }),
110
+ events.todoCreated({ id: 't2', text: 'buy eggs', completed: false }),
112
111
  )
113
112
 
114
113
  const { result } = ReactTesting.renderHook(
115
114
  () => {
116
115
  renderCount.inc()
117
116
 
118
- return LiveStoreReact.useQuery(todo$)[0]!.text
117
+ return store.useQuery(todo$)[0]!.text
119
118
  },
120
119
  { wrapper },
121
120
  )
@@ -124,7 +123,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
124
123
  expect(renderCount.val).toBe(1)
125
124
  expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
126
125
 
127
- ReactTesting.act(() => store.commit(todos.update({ where: { id: 't1' }, values: { text: 'buy soy milk' } })))
126
+ ReactTesting.act(() => store.commit(events.todoUpdated({ id: 't1', text: 'buy soy milk' })))
128
127
 
129
128
  expect(result.current).toBe('buy soy milk')
130
129
  expect(renderCount.val).toBe(2)
@@ -143,7 +142,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
143
142
  // To handle this properly we introduced the `_tag: 'destroyed'` state in the `spanAlreadyStartedCache`.
144
143
  Vitest.scopedLive('should work for a list with react-window', () =>
145
144
  Effect.gen(function* () {
146
- const { wrapper } = yield* makeTodoMvcReact({ strictMode })
145
+ const { wrapper, store } = yield* makeTodoMvcReact({ strictMode })
147
146
 
148
147
  const ListWrapper: React.FC<{ numItems: number }> = ({ numItems }) => {
149
148
  return (
@@ -161,7 +160,7 @@ Vitest.describe.each([{ strictMode: true }, { strictMode: false }] as const)(
161
160
 
162
161
  const ListItem: React.FC<{ data: ReadonlyArray<number>; index: number }> = ({ data: ids, index }) => {
163
162
  const id = ids[index]!
164
- const res = LiveStoreReact.useQuery(LiveStore.computed(() => id, { label: `ListItem.${id}`, deps: id }))
163
+ const res = store.useQuery(LiveStore.computed(() => id, { label: `ListItem.${id}`, deps: id }))
165
164
  return <div role="listitem">{res}</div>
166
165
  }
167
166
 
package/src/useQuery.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
2
2
  import { extractStackInfoFromStackTrace, stackInfoToString } from '@livestore/livestore'
3
3
  import type { LiveQueries } from '@livestore/livestore/internal'
4
- import { deepEqual, indent } from '@livestore/utils'
4
+ import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
5
5
  import * as otel from '@opentelemetry/api'
6
6
  import React from 'react'
7
7
 
8
- import { useStore } from './LiveStoreContext.js'
8
+ import { LiveStoreContext } from './LiveStoreContext.js'
9
9
  import { useRcResource } from './useRcResource.js'
10
10
  import { originalStackLimit } from './utils/stack-info.js'
11
11
  import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
@@ -26,9 +26,6 @@ export const useQuery = <TQuery extends LiveQueryDef.Any>(
26
26
  options?: { store?: Store },
27
27
  ): LiveQueries.GetResult<TQuery> => useQueryRef(queryDef, options).valueRef.current
28
28
 
29
- type GetQueryInfo<TQuery extends LiveQueryDef.Any> =
30
- TQuery extends LiveQueryDef<infer _1, infer TQueryInfo> ? TQueryInfo : never
31
-
32
29
  /**
33
30
  */
34
31
  export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
@@ -42,9 +39,13 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
42
39
  },
43
40
  ): {
44
41
  valueRef: React.RefObject<LiveQueries.GetResult<TQuery>>
45
- queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>, GetQueryInfo<TQuery>>>
42
+ queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>>>
46
43
  } => {
47
- const { store } = useStore({ store: options?.store })
44
+ const store =
45
+ options?.store ??
46
+ // eslint-disable-next-line react-hooks/rules-of-hooks
47
+ React.useContext(LiveStoreContext)?.store ??
48
+ shouldNeverHappen(`No store provided to useQuery`)
48
49
 
49
50
  const rcRefKey = `${store.storeId}_${queryDef.hash}`
50
51
 
@@ -77,7 +78,7 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
77
78
  // which takes care of disposing the queryRcRef
78
79
  () => {},
79
80
  )
80
- const query$ = queryRcRef.value as LiveQuery<LiveQueries.GetResult<TQuery>, GetQueryInfo<TQuery>>
81
+ const query$ = queryRcRef.value as LiveQuery<LiveQueries.GetResult<TQuery>>
81
82
 
82
83
  React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
83
84
  // console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
@@ -0,0 +1,36 @@
1
+ import type { Store } from '@livestore/livestore'
2
+ import React from 'react'
3
+
4
+ import type { ReactApi } from './LiveStoreContext.js'
5
+ import { LiveStoreContext } from './LiveStoreContext.js'
6
+ import { useClientDocument } from './useClientDocument.js'
7
+ import { useQuery } from './useQuery.js'
8
+
9
+ export const withReactApi = (store: Store): Store & ReactApi => {
10
+ // @ts-expect-error TODO properly implement this
11
+ // eslint-disable-next-line react-hooks/rules-of-hooks
12
+ store.useQuery = (queryDef) => useQuery(queryDef, { store })
13
+ // @ts-expect-error TODO properly implement this
14
+ // eslint-disable-next-line react-hooks/rules-of-hooks
15
+ store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
16
+ return store as Store & ReactApi
17
+ }
18
+
19
+ export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {
20
+ if (options?.store !== undefined) {
21
+ return { store: withReactApi(options.store) }
22
+ }
23
+
24
+ // eslint-disable-next-line react-hooks/rules-of-hooks
25
+ const storeContext = React.useContext(LiveStoreContext)
26
+
27
+ if (storeContext === undefined) {
28
+ throw new Error(`useStore can only be used inside StoreContext.Provider`)
29
+ }
30
+
31
+ if (storeContext.stage !== 'running') {
32
+ throw new Error(`useStore can only be used after the store is running`)
33
+ }
34
+
35
+ return { store: withReactApi(storeContext.store) }
36
+ }
package/tmp/pack.tgz CHANGED
Binary file