@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/LiveStoreContext.d.ts +10 -6
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +0 -14
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +2 -2
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +5 -1
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/LiveStoreProvider.test.js +6 -5
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +115 -546
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +64 -22
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/mod.d.ts +4 -4
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +61 -0
- package/dist/useClientDocument.d.ts.map +1 -0
- package/dist/useClientDocument.js +79 -0
- package/dist/useClientDocument.js.map +1 -0
- package/dist/useClientDocument.test.d.ts +2 -0
- package/dist/useClientDocument.test.d.ts.map +1 -0
- package/dist/{useRow.test.js → useClientDocument.test.js} +44 -48
- package/dist/useClientDocument.test.js.map +1 -0
- package/dist/useQuery.d.ts +1 -3
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +6 -3
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +16 -17
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useStore.d.ts +9 -0
- package/dist/useStore.d.ts.map +1 -0
- package/dist/useStore.js +28 -0
- package/dist/useStore.js.map +1 -0
- package/package.json +11 -11
- package/src/LiveStoreContext.ts +10 -19
- package/src/LiveStoreProvider.test.tsx +6 -5
- package/src/LiveStoreProvider.tsx +7 -4
- package/src/__snapshots__/{useRow.test.tsx.snap → useClientDocument.test.tsx.snap} +62 -46
- package/src/__snapshots__/useQuery.test.tsx.snap +8 -8
- package/src/__tests__/fixture.tsx +69 -39
- package/src/mod.ts +5 -5
- package/src/{useRow.test.tsx → useClientDocument.test.tsx} +51 -55
- package/src/useClientDocument.ts +157 -0
- package/src/useQuery.test.tsx +18 -19
- package/src/useQuery.ts +9 -8
- package/src/useStore.ts +36 -0
- package/tmp/pack.tgz +0 -0
- package/dist/useAtom.d.ts +0 -8
- package/dist/useAtom.d.ts.map +0 -1
- package/dist/useAtom.js +0 -42
- package/dist/useAtom.js.map +0 -1
- package/dist/useRow.d.ts +0 -64
- package/dist/useRow.d.ts.map +0 -1
- package/dist/useRow.js +0 -108
- package/dist/useRow.js.map +0 -1
- package/dist/useRow.test.d.ts +0 -2
- package/dist/useRow.test.d.ts.map +0 -1
- package/dist/useRow.test.js.map +0 -1
- package/src/useAtom.ts +0 -66
- 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 {
|
|
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('
|
|
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] =
|
|
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.
|
|
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.
|
|
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.
|
|
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] =
|
|
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.
|
|
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
|
|
72
|
+
ReactTesting.act(() => result.current.setState({ username: 'username_u1_hello' }))
|
|
73
73
|
|
|
74
|
-
expect(result.current.
|
|
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
|
|
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] =
|
|
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.
|
|
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.
|
|
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.
|
|
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] =
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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$] =
|
|
208
|
-
const todos =
|
|
201
|
+
const [_row, _setRow, _id, rowState$] = store.useClientDocument(tables.userInfo, userId)
|
|
202
|
+
const todos = store.useQuery(
|
|
209
203
|
LiveStore.queryDb(
|
|
210
|
-
(get) => tables.todos.
|
|
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(
|
|
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
|
-
|
|
247
|
-
|
|
237
|
+
const provider = new BasicTracerProvider({
|
|
238
|
+
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
239
|
+
})
|
|
248
240
|
|
|
249
|
-
const otelTracer =
|
|
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] =
|
|
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.
|
|
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(
|
|
269
|
+
ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
|
|
278
270
|
|
|
279
271
|
rerender('u2')
|
|
280
272
|
|
|
281
|
-
expect(result.current.
|
|
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
|
+
}
|
package/src/useQuery.test.tsx
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
106
|
+
const todo$ = queryDb((get) => tables.todos.where('id', get(filter$)), { label: 'todo' })
|
|
108
107
|
|
|
109
108
|
store.commit(
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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 {
|
|
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
|
|
42
|
+
queryRcRef: LiveQueries.RcRef<LiveQuery<LiveQueries.GetResult<TQuery>>>
|
|
46
43
|
} => {
|
|
47
|
-
const
|
|
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
|
|
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}`)
|
package/src/useStore.ts
ADDED
|
@@ -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
|