@livestore/react 0.0.0-snapshot-2ef046b02334f52613d31dbe06af53487685edc0 → 0.0.0-snapshot-2c861249e50661661613204300b1fc0d902c2e46
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 +73 -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} +38 -43
- 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 +5 -5
- 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} +50 -30
- 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} +43 -49
- package/src/useClientDocument.ts +149 -0
- package/src/useQuery.test.tsx +18 -19
- package/src/useQuery.ts +9 -8
- package/src/useStore.ts +36 -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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
2
|
import { provideOtel } from '@livestore/common'
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
3
|
+
import { Events, makeSchema, State } from '@livestore/common/schema'
|
|
4
|
+
import type { Store } from '@livestore/livestore'
|
|
5
5
|
import { createStore } from '@livestore/livestore'
|
|
6
|
-
import { Effect } from '@livestore/utils/effect'
|
|
6
|
+
import { Effect, Schema } from '@livestore/utils/effect'
|
|
7
7
|
import type * as otel from '@opentelemetry/api'
|
|
8
8
|
import React from 'react'
|
|
9
9
|
|
|
@@ -22,45 +22,70 @@ export type AppState = {
|
|
|
22
22
|
filter: Filter
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
'todos',
|
|
27
|
-
{
|
|
28
|
-
id:
|
|
29
|
-
text:
|
|
30
|
-
completed:
|
|
25
|
+
const todos = State.SQLite.table({
|
|
26
|
+
name: 'todos',
|
|
27
|
+
columns: {
|
|
28
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
29
|
+
text: State.SQLite.text({ default: '', nullable: false }),
|
|
30
|
+
completed: State.SQLite.boolean({ default: false, nullable: false }),
|
|
31
31
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
filter: DbSchema.text({ default: 'all', nullable: false }),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const app = State.SQLite.table({
|
|
35
|
+
name: 'app',
|
|
36
|
+
columns: {
|
|
37
|
+
id: State.SQLite.text({ primaryKey: true, default: 'static' }),
|
|
38
|
+
newTodoText: State.SQLite.text({ default: '', nullable: true }),
|
|
39
|
+
filter: State.SQLite.text({ default: 'all', nullable: false }),
|
|
41
40
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const userInfo = State.SQLite.clientDocument({
|
|
44
|
+
name: 'UserInfo',
|
|
45
|
+
schema: Schema.Struct({
|
|
46
|
+
username: Schema.String,
|
|
47
|
+
text: Schema.String,
|
|
48
|
+
}),
|
|
49
|
+
default: { value: { username: '', text: '' } },
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const AppRouterSchema = State.SQLite.clientDocument({
|
|
53
|
+
name: 'AppRouter',
|
|
54
|
+
schema: Schema.Struct({
|
|
55
|
+
currentTaskId: Schema.String.pipe(Schema.NullOr),
|
|
56
|
+
}),
|
|
57
|
+
default: {
|
|
58
|
+
value: { currentTaskId: null },
|
|
59
|
+
id: 'singleton',
|
|
50
60
|
},
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export const events = {
|
|
64
|
+
todoCreated: Events.synced({
|
|
65
|
+
name: 'todoCreated',
|
|
66
|
+
schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean }),
|
|
67
|
+
}),
|
|
68
|
+
todoUpdated: Events.synced({
|
|
69
|
+
name: 'todoUpdated',
|
|
70
|
+
schema: Schema.Struct({
|
|
71
|
+
id: Schema.String,
|
|
72
|
+
text: Schema.String.pipe(Schema.optional),
|
|
73
|
+
completed: Schema.Boolean.pipe(Schema.optional),
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
AppRouterSet: AppRouterSchema.set,
|
|
77
|
+
UserInfoSet: userInfo.set,
|
|
78
|
+
}
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
{ isSingleton: true, deriveMutations: { clientOnly: true } },
|
|
60
|
-
)
|
|
80
|
+
const materializers = State.SQLite.materializers(events, {
|
|
81
|
+
todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
|
|
82
|
+
todoUpdated: ({ id, text, completed }) => todos.update({ completed, text }).where({ id }),
|
|
83
|
+
})
|
|
61
84
|
|
|
62
85
|
export const tables = { todos, app, userInfo, AppRouterSchema }
|
|
63
|
-
|
|
86
|
+
|
|
87
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
88
|
+
export const schema = makeSchema({ state, events })
|
|
64
89
|
|
|
65
90
|
export const makeTodoMvcReact = ({
|
|
66
91
|
otelTracer,
|
|
@@ -87,15 +112,20 @@ export const makeTodoMvcReact = ({
|
|
|
87
112
|
}
|
|
88
113
|
}
|
|
89
114
|
|
|
90
|
-
const store = yield* createStore({
|
|
115
|
+
const store: Store<any> = yield* createStore({
|
|
91
116
|
schema,
|
|
92
117
|
storeId: 'default',
|
|
93
118
|
adapter: makeInMemoryAdapter(),
|
|
94
119
|
debug: { instanceId: 'test' },
|
|
95
120
|
})
|
|
96
121
|
|
|
122
|
+
const storeWithReactApi = LiveStoreReact.withReactApi(store)
|
|
123
|
+
|
|
97
124
|
// TODO improve typing of `LiveStoreContext`
|
|
98
|
-
const storeContext = {
|
|
125
|
+
const storeContext = {
|
|
126
|
+
stage: 'running' as const,
|
|
127
|
+
store: storeWithReactApi,
|
|
128
|
+
}
|
|
99
129
|
|
|
100
130
|
const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
101
131
|
|
|
@@ -109,5 +139,5 @@ export const makeTodoMvcReact = ({
|
|
|
109
139
|
|
|
110
140
|
const renderCount = makeRenderCount()
|
|
111
141
|
|
|
112
|
-
return { wrapper, store, renderCount }
|
|
142
|
+
return { wrapper, store: storeWithReactApi, renderCount }
|
|
113
143
|
}).pipe(provideOtel({ parentSpanContext: otelContext, otelTracer }))
|
package/src/mod.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export { LiveStoreContext,
|
|
1
|
+
export { LiveStoreContext, type ReactApi } from './LiveStoreContext.js'
|
|
2
|
+
export { useStore, withReactApi } from './useStore.js'
|
|
2
3
|
export { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
3
|
-
export { useQuery } from './useQuery.js'
|
|
4
4
|
export { useStackInfo } from './utils/stack-info.js'
|
|
5
|
+
export { useQuery, useQueryRef } from './useQuery.js'
|
|
5
6
|
export {
|
|
6
|
-
|
|
7
|
+
useClientDocument,
|
|
7
8
|
type StateSetters,
|
|
8
9
|
type SetStateAction,
|
|
9
10
|
type Dispatch,
|
|
10
11
|
type UseRowResult as UseStateResult,
|
|
11
|
-
} from './
|
|
12
|
-
export { useAtom } from './useAtom.js'
|
|
12
|
+
} from './useClientDocument.js'
|
|
@@ -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)
|
|
@@ -262,23 +256,23 @@ Vitest.describe('useRow', () => {
|
|
|
262
256
|
(userId: string) => {
|
|
263
257
|
renderCount.inc()
|
|
264
258
|
|
|
265
|
-
const [state, setState] =
|
|
266
|
-
return { state, setState }
|
|
259
|
+
const [state, setState, id] = store.useClientDocument(tables.userInfo, userId)
|
|
260
|
+
return { state, setState, id }
|
|
267
261
|
},
|
|
268
262
|
{ wrapper, initialProps: 'u1' },
|
|
269
263
|
)
|
|
270
264
|
|
|
271
|
-
expect(result.current.
|
|
265
|
+
expect(result.current.id).toBe('u1')
|
|
272
266
|
expect(result.current.state.username).toBe('')
|
|
273
267
|
expect(renderCount.val).toBe(1)
|
|
274
268
|
|
|
275
269
|
// For u2 we'll make sure that the row already exists,
|
|
276
270
|
// so the lazy `insert` will be skipped
|
|
277
|
-
ReactTesting.act(() => store.commit(
|
|
271
|
+
ReactTesting.act(() => store.commit(events.UserInfoSet({ username: 'username_u2' }, 'u2')))
|
|
278
272
|
|
|
279
273
|
rerender('u2')
|
|
280
274
|
|
|
281
|
-
expect(result.current.
|
|
275
|
+
expect(result.current.id).toBe('u2')
|
|
282
276
|
expect(result.current.state.username).toBe('username_u2')
|
|
283
277
|
expect(renderCount.val).toBe(2)
|
|
284
278
|
|
|
@@ -0,0 +1,149 @@
|
|
|
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(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
|
+
}
|