@livestore/react 0.4.0-dev.9 → 0.5.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +12 -283
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +12 -86
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +11 -9
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +8 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +6 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +12 -4
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -18
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +28 -20
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +29 -8
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +43 -70
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +67 -23
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +42 -35
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +73 -50
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +74 -7
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +82 -16
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts +2 -0
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/useStore.test.js +242 -0
- package/dist/useStore.test.js.map +1 -0
- package/dist/useSyncStatus.d.ts +22 -0
- package/dist/useSyncStatus.d.ts.map +1 -0
- package/dist/useSyncStatus.js +28 -0
- package/dist/useSyncStatus.js.map +1 -0
- package/package.json +60 -27
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.test.tsx.snap +152 -78
- package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
- package/src/__tests__/fixture.tsx +32 -135
- package/src/experimental/components/LiveList.tsx +25 -11
- package/src/mod.ts +8 -12
- package/src/useClientDocument.test.tsx +38 -23
- package/src/useClientDocument.ts +19 -38
- package/src/useQuery.test.tsx +110 -23
- package/src/useQuery.ts +74 -89
- package/src/useRcResource.test.tsx +116 -59
- package/src/useRcResource.ts +52 -38
- package/src/useStore.test.tsx +357 -0
- package/src/useStore.ts +115 -22
- package/src/useSyncStatus.ts +34 -0
- package/dist/LiveStoreContext.d.ts +0 -13
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -3
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -65
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -221
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- package/dist/utils/stack-info.d.ts +0 -4
- package/dist/utils/stack-info.d.ts.map +0 -1
- package/dist/utils/stack-info.js +0 -10
- package/dist/utils/stack-info.js.map +0 -1
- package/src/LiveStoreContext.ts +0 -14
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -413
- package/src/ambient.d.ts +0 -1
- package/src/utils/stack-info.ts +0 -13
|
@@ -1,131 +1,45 @@
|
|
|
1
|
-
import { makeInMemoryAdapter } from '@livestore/adapter-web'
|
|
2
|
-
import { provideOtel, type UnexpectedError } from '@livestore/common'
|
|
3
|
-
import { Events, makeSchema, State } from '@livestore/common/schema'
|
|
4
|
-
import type { LiveStoreSchema, SqliteDsl, Store } from '@livestore/livestore'
|
|
5
|
-
import { createStore } from '@livestore/livestore'
|
|
6
|
-
import { omitUndefineds } from '@livestore/utils'
|
|
7
|
-
import { Effect, Schema, type Scope } from '@livestore/utils/effect'
|
|
8
|
-
import type * as otel from '@opentelemetry/api'
|
|
9
1
|
import React from 'react'
|
|
10
2
|
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
import type { UnknownError } from '@livestore/common'
|
|
4
|
+
import {
|
|
5
|
+
type AppState,
|
|
6
|
+
type CreateTodoMvcStoreOptions,
|
|
7
|
+
createTodoMvcStore,
|
|
8
|
+
events,
|
|
9
|
+
type Filter,
|
|
10
|
+
schema,
|
|
11
|
+
type Todo,
|
|
12
|
+
tables,
|
|
13
|
+
} from '@livestore/framework-toolkit/testing'
|
|
14
|
+
import type { Store } from '@livestore/livestore'
|
|
15
|
+
import { Effect, type Scope } from '@livestore/utils/effect'
|
|
16
|
+
|
|
17
|
+
import * as LiveStoreReact from '../mod.ts'
|
|
18
|
+
|
|
19
|
+
export type { AppState, Filter, Todo }
|
|
20
|
+
// Re-export shared types and schema
|
|
21
|
+
export { events, schema, tables }
|
|
22
|
+
|
|
23
|
+
export type MakeTodoMvcReactOptions = CreateTodoMvcStoreOptions & {
|
|
24
|
+
strictMode?: boolean | undefined
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
export type AppState = {
|
|
22
|
-
newTodoText: string
|
|
23
|
-
filter: Filter
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const todos = State.SQLite.table({
|
|
27
|
-
name: 'todos',
|
|
28
|
-
columns: {
|
|
29
|
-
id: State.SQLite.text({ primaryKey: true }),
|
|
30
|
-
text: State.SQLite.text({ default: '', nullable: false }),
|
|
31
|
-
completed: State.SQLite.boolean({ default: false, nullable: false }),
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
const app = State.SQLite.table({
|
|
36
|
-
name: 'app',
|
|
37
|
-
columns: {
|
|
38
|
-
id: State.SQLite.text({ primaryKey: true, default: 'static' }),
|
|
39
|
-
newTodoText: State.SQLite.text({ default: '', nullable: true }),
|
|
40
|
-
filter: State.SQLite.text({ default: 'all', nullable: false }),
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const userInfo = State.SQLite.clientDocument({
|
|
45
|
-
name: 'UserInfo',
|
|
46
|
-
schema: Schema.Struct({
|
|
47
|
-
username: Schema.String,
|
|
48
|
-
text: Schema.String,
|
|
49
|
-
}),
|
|
50
|
-
default: { value: { username: '', text: '' } },
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const AppRouterSchema = State.SQLite.clientDocument({
|
|
54
|
-
name: 'AppRouter',
|
|
55
|
-
schema: Schema.Struct({
|
|
56
|
-
currentTaskId: Schema.String.pipe(Schema.NullOr),
|
|
57
|
-
}),
|
|
58
|
-
default: {
|
|
59
|
-
value: { currentTaskId: null },
|
|
60
|
-
id: 'singleton',
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const kv = State.SQLite.clientDocument({
|
|
65
|
-
name: 'Kv',
|
|
66
|
-
schema: Schema.Any,
|
|
67
|
-
default: { value: null },
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
export const events = {
|
|
71
|
-
todoCreated: Events.synced({
|
|
72
|
-
name: 'todoCreated',
|
|
73
|
-
schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean }),
|
|
74
|
-
}),
|
|
75
|
-
todoUpdated: Events.synced({
|
|
76
|
-
name: 'todoUpdated',
|
|
77
|
-
schema: Schema.Struct({
|
|
78
|
-
id: Schema.String,
|
|
79
|
-
text: Schema.String.pipe(Schema.optional),
|
|
80
|
-
completed: Schema.Boolean.pipe(Schema.optional),
|
|
81
|
-
}),
|
|
82
|
-
}),
|
|
83
|
-
AppRouterSet: AppRouterSchema.set,
|
|
84
|
-
UserInfoSet: userInfo.set,
|
|
85
|
-
KvSet: kv.set,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const materializers = State.SQLite.materializers(events, {
|
|
89
|
-
todoCreated: ({ id, text, completed }) => todos.insert({ id, text, completed }),
|
|
90
|
-
todoUpdated: ({ id, text, completed }) => todos.update({ ...omitUndefineds({ completed, text }) }).where({ id }),
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
export const tables = { todos, app, userInfo, AppRouterSchema, kv }
|
|
94
|
-
|
|
95
|
-
const state = State.SQLite.makeState({ tables, materializers })
|
|
96
|
-
export const schema = makeSchema({ state, events })
|
|
97
|
-
|
|
98
|
-
export const makeTodoMvcReact: ({
|
|
99
|
-
otelTracer,
|
|
100
|
-
otelContext,
|
|
101
|
-
strictMode,
|
|
102
|
-
}?: {
|
|
103
|
-
otelTracer?: otel.Tracer
|
|
104
|
-
otelContext?: otel.Context
|
|
105
|
-
strictMode?: boolean
|
|
106
|
-
}) => Effect.Effect<
|
|
27
|
+
export const makeTodoMvcReact: (opts?: MakeTodoMvcReactOptions) => Effect.Effect<
|
|
107
28
|
{
|
|
108
29
|
wrapper: ({ children }: any) => React.JSX.Element
|
|
109
|
-
store: Store<
|
|
30
|
+
store: Store<typeof schema> & LiveStoreReact.ReactApi
|
|
110
31
|
renderCount: { readonly val: number; inc: () => void }
|
|
111
32
|
},
|
|
112
|
-
|
|
33
|
+
UnknownError,
|
|
113
34
|
Scope.Scope
|
|
114
|
-
> = ({
|
|
115
|
-
otelTracer,
|
|
116
|
-
otelContext,
|
|
117
|
-
strictMode,
|
|
118
|
-
}: {
|
|
119
|
-
otelTracer?: otel.Tracer
|
|
120
|
-
otelContext?: otel.Context
|
|
121
|
-
strictMode?: boolean
|
|
122
|
-
} = {}) =>
|
|
35
|
+
> = (opts: MakeTodoMvcReactOptions = {}) =>
|
|
123
36
|
Effect.gen(function* () {
|
|
37
|
+
const { strictMode } = opts
|
|
124
38
|
const makeRenderCount = () => {
|
|
125
39
|
let val = 0
|
|
126
40
|
|
|
127
41
|
const inc = () => {
|
|
128
|
-
val += strictMode ? 0.5 : 1
|
|
42
|
+
val += strictMode === true ? 0.5 : 1
|
|
129
43
|
}
|
|
130
44
|
|
|
131
45
|
return {
|
|
@@ -136,32 +50,15 @@ export const makeTodoMvcReact: ({
|
|
|
136
50
|
}
|
|
137
51
|
}
|
|
138
52
|
|
|
139
|
-
const store
|
|
140
|
-
schema,
|
|
141
|
-
storeId: 'default',
|
|
142
|
-
adapter: makeInMemoryAdapter(),
|
|
143
|
-
debug: { instanceId: 'test' },
|
|
144
|
-
})
|
|
53
|
+
const store = yield* createTodoMvcStore(opts)
|
|
145
54
|
|
|
146
55
|
const storeWithReactApi = LiveStoreReact.withReactApi(store)
|
|
147
56
|
|
|
148
|
-
|
|
149
|
-
const storeContext = {
|
|
150
|
-
stage: 'running' as const,
|
|
151
|
-
store: storeWithReactApi,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const MaybeStrictMode = strictMode ? React.StrictMode : React.Fragment
|
|
57
|
+
const MaybeStrictMode = strictMode === true ? React.StrictMode : React.Fragment
|
|
155
58
|
|
|
156
|
-
const wrapper = ({ children }: any) =>
|
|
157
|
-
<MaybeStrictMode>
|
|
158
|
-
<LiveStoreReact.LiveStoreContext.Provider value={storeContext}>
|
|
159
|
-
{children}
|
|
160
|
-
</LiveStoreReact.LiveStoreContext.Provider>
|
|
161
|
-
</MaybeStrictMode>
|
|
162
|
-
)
|
|
59
|
+
const wrapper = ({ children }: any) => <MaybeStrictMode>{children}</MaybeStrictMode>
|
|
163
60
|
|
|
164
61
|
const renderCount = makeRenderCount()
|
|
165
62
|
|
|
166
63
|
return { wrapper, store: storeWithReactApi, renderCount }
|
|
167
|
-
})
|
|
64
|
+
})
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { LiveQueryDef } from '@livestore/livestore'
|
|
2
|
-
import { computed } from '@livestore/livestore'
|
|
3
1
|
import React from 'react'
|
|
4
2
|
|
|
5
|
-
import {
|
|
3
|
+
import type { LiveQueryDef, Store } from '@livestore/livestore'
|
|
4
|
+
import { computed } from '@livestore/livestore'
|
|
5
|
+
|
|
6
|
+
import { useQuery } from '../../useQuery.ts'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
TODO:
|
|
@@ -16,6 +17,8 @@ export type LiveListProps<TItem> = {
|
|
|
16
17
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
17
18
|
/** Needs to be unique across all list items */
|
|
18
19
|
getKey: (item: TItem, index: number) => string | number
|
|
20
|
+
/** The store instance to use for queries */
|
|
21
|
+
store: Store<any, any>
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -26,12 +29,15 @@ export type LiveListProps<TItem> = {
|
|
|
26
29
|
* In the future we want to make this component even more efficient by using incremental rendering (https://github.com/livestorejs/livestore/pull/55)
|
|
27
30
|
* e.g. when an item is added/removed/moved to only re-render the affected DOM nodes.
|
|
28
31
|
*/
|
|
29
|
-
export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<TItem>): React.ReactNode => {
|
|
32
|
+
export const LiveList = <TItem,>({ items$, renderItem, getKey, store }: LiveListProps<TItem>): React.ReactNode => {
|
|
30
33
|
const [hasMounted, setHasMounted] = React.useState(false)
|
|
31
34
|
|
|
32
35
|
React.useEffect(() => setHasMounted(true), [])
|
|
33
36
|
|
|
34
|
-
const keys = useQuery(
|
|
37
|
+
const keys = useQuery(
|
|
38
|
+
computed((get) => get(items$).map(getKey)),
|
|
39
|
+
{ store },
|
|
40
|
+
)
|
|
35
41
|
const arr = React.useMemo(
|
|
36
42
|
() =>
|
|
37
43
|
keys.map(
|
|
@@ -54,7 +60,9 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
54
60
|
key={key}
|
|
55
61
|
itemKey={key}
|
|
56
62
|
item$={item$}
|
|
57
|
-
|
|
63
|
+
store={store}
|
|
64
|
+
index={index}
|
|
65
|
+
isInitialListRender={!hasMounted}
|
|
58
66
|
renderItem={renderItem}
|
|
59
67
|
/>
|
|
60
68
|
))}
|
|
@@ -64,15 +72,20 @@ export const LiveList = <TItem,>({ items$, renderItem, getKey }: LiveListProps<T
|
|
|
64
72
|
|
|
65
73
|
const ItemWrapper = <TItem,>({
|
|
66
74
|
item$,
|
|
67
|
-
|
|
75
|
+
index,
|
|
76
|
+
isInitialListRender,
|
|
68
77
|
renderItem,
|
|
78
|
+
store,
|
|
69
79
|
}: {
|
|
70
80
|
itemKey: string | number
|
|
71
81
|
item$: LiveQueryDef<TItem>
|
|
72
|
-
|
|
82
|
+
index: number
|
|
83
|
+
isInitialListRender: boolean
|
|
73
84
|
renderItem: (item: TItem, opts: { index: number; isInitialListRender: boolean }) => React.ReactNode
|
|
85
|
+
store: Store<any, any>
|
|
74
86
|
}) => {
|
|
75
|
-
const item = useQuery(item
|
|
87
|
+
const item = useQuery(item$, { store })
|
|
88
|
+
const opts = React.useMemo(() => ({ index, isInitialListRender }), [index, isInitialListRender])
|
|
76
89
|
|
|
77
90
|
return <>{renderItem(item, opts)}</>
|
|
78
91
|
}
|
|
@@ -82,6 +95,7 @@ const ItemWrapperMemo = React.memo(
|
|
|
82
95
|
(prev, next) =>
|
|
83
96
|
prev.itemKey === next.itemKey &&
|
|
84
97
|
prev.renderItem === next.renderItem &&
|
|
85
|
-
prev.
|
|
86
|
-
prev.
|
|
98
|
+
prev.store === next.store &&
|
|
99
|
+
prev.index === next.index &&
|
|
100
|
+
prev.isInitialListRender === next.isInitialListRender,
|
|
87
101
|
) as typeof ItemWrapper
|
package/src/mod.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type StateSetters,
|
|
8
|
-
type UseClientDocumentResult,
|
|
9
|
-
useClientDocument,
|
|
10
|
-
} from './useClientDocument.ts'
|
|
1
|
+
export type { Dispatch, SetStateAction, SetStateActionPartial, StateSetters } from '@livestore/framework-toolkit'
|
|
2
|
+
export { captureStackInfo } from '@livestore/framework-toolkit'
|
|
3
|
+
export { StoreRegistry, storeOptions } from '@livestore/livestore'
|
|
4
|
+
export { LiveList, type LiveListProps } from './experimental/components/LiveList.tsx'
|
|
5
|
+
export * from './StoreRegistryContext.tsx'
|
|
6
|
+
export { type UseClientDocumentResult, useClientDocument } from './useClientDocument.ts'
|
|
11
7
|
export { useQuery, useQueryRef } from './useQuery.ts'
|
|
12
|
-
export { useStore, withReactApi } from './useStore.ts'
|
|
13
|
-
export {
|
|
8
|
+
export { type ReactApi, useStore, withReactApi } from './useStore.ts'
|
|
9
|
+
export { useSyncStatus } from './useSyncStatus.ts'
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
/** biome-ignore-all lint/a11y/useValidAriaRole: not needed for testing */
|
|
2
|
-
/** biome-ignore-all lint/a11y/noStaticElementInteractions: not needed for testing */
|
|
3
|
-
import * as LiveStore from '@livestore/livestore'
|
|
4
|
-
import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
5
|
-
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
6
|
-
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
1
|
import * as otel from '@opentelemetry/api'
|
|
8
2
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
9
3
|
import * as ReactTesting from '@testing-library/react'
|
|
10
|
-
import
|
|
4
|
+
import * as React from 'react'
|
|
11
5
|
import { beforeEach, expect, it } from 'vitest'
|
|
12
6
|
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
7
|
+
import * as LiveStore from '@livestore/livestore'
|
|
8
|
+
import { StoreInternalsSymbol } from '@livestore/livestore'
|
|
9
|
+
import { getAllSimplifiedRootSpans, getSimplifiedRootSpan } from '@livestore/livestore/internal/testing-utils'
|
|
10
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
11
|
+
import { Effect, ReadonlyRecord, Schema } from '@livestore/utils/effect'
|
|
12
|
+
|
|
13
|
+
import { events, makeTodoMvcReact, tables } from './__tests__/fixture.tsx'
|
|
14
|
+
import type * as LiveStoreReact from './mod.ts'
|
|
15
|
+
import { __resetUseRcResourceCache } from './useRcResource.ts'
|
|
16
16
|
|
|
17
17
|
// const strictMode = process.env.REACT_STRICT_MODE !== undefined
|
|
18
18
|
|
|
19
19
|
// NOTE running tests concurrently doesn't work with the default global db graph
|
|
20
|
+
/** Verifies: LS.SYS.INT.REACT-R01, LS.SYS.STATE.SQLITE-R07 */
|
|
20
21
|
Vitest.describe('useClientDocument', () => {
|
|
21
22
|
beforeEach(() => {
|
|
22
23
|
__resetUseRcResourceCache()
|
|
23
24
|
})
|
|
24
25
|
|
|
25
|
-
Vitest.
|
|
26
|
+
Vitest.live('should update the data based on component key', () =>
|
|
26
27
|
Effect.gen(function* () {
|
|
27
28
|
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
28
29
|
|
|
@@ -39,12 +40,12 @@ Vitest.describe('useClientDocument', () => {
|
|
|
39
40
|
expect(result.current.id).toBe('u1')
|
|
40
41
|
expect(result.current.state.username).toBe('')
|
|
41
42
|
expect(renderCount.val).toBe(1)
|
|
42
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
43
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
43
44
|
store.commit(tables.userInfo.set({ username: 'username_u2' }, 'u2'))
|
|
44
45
|
|
|
45
46
|
rerender('u2')
|
|
46
47
|
|
|
47
|
-
expect(store.reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
48
|
+
expect(store[StoreInternalsSymbol].reactivityGraph.getSnapshot({ includeResults: true })).toMatchSnapshot()
|
|
48
49
|
expect(result.current.id).toBe('u2')
|
|
49
50
|
expect(result.current.state.username).toBe('username_u2')
|
|
50
51
|
expect(renderCount.val).toBe(2)
|
|
@@ -53,7 +54,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
53
54
|
|
|
54
55
|
// TODO add a test that makes sure React doesn't re-render when a setter is used to set the same value
|
|
55
56
|
|
|
56
|
-
Vitest.
|
|
57
|
+
Vitest.live('should update the data reactively - via setState', () =>
|
|
57
58
|
Effect.gen(function* () {
|
|
58
59
|
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
59
60
|
|
|
@@ -79,7 +80,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
79
80
|
}),
|
|
80
81
|
)
|
|
81
82
|
|
|
82
|
-
Vitest.
|
|
83
|
+
Vitest.live('should update the data reactively - via raw store commit', () =>
|
|
83
84
|
Effect.gen(function* () {
|
|
84
85
|
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
85
86
|
|
|
@@ -105,7 +106,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
105
106
|
}),
|
|
106
107
|
)
|
|
107
108
|
|
|
108
|
-
Vitest.
|
|
109
|
+
Vitest.live('should work for a larger app', () =>
|
|
109
110
|
Effect.gen(function* () {
|
|
110
111
|
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
111
112
|
|
|
@@ -119,25 +120,39 @@ Vitest.describe('useClientDocument', () => {
|
|
|
119
120
|
renderCount.inc()
|
|
120
121
|
|
|
121
122
|
const [state, setState] = store.useClientDocument(tables.AppRouterSchema, 'singleton')
|
|
123
|
+
const setCurrentTaskId = React.useCallback((taskId: string) => setState({ currentTaskId: taskId }), [setState])
|
|
122
124
|
|
|
123
125
|
globalSetState = setState
|
|
124
126
|
|
|
125
127
|
return (
|
|
126
128
|
<div>
|
|
127
|
-
<TasksList setTaskId={
|
|
129
|
+
<TasksList setTaskId={setCurrentTaskId} />
|
|
128
130
|
<div role="current-id">Current Task Id: {state.currentTaskId ?? '-'}</div>
|
|
129
|
-
{state.currentTaskId
|
|
131
|
+
{state.currentTaskId !== null ? (
|
|
132
|
+
<TaskDetails id={state.currentTaskId} />
|
|
133
|
+
) : (
|
|
134
|
+
<div>Click on a task to see details</div>
|
|
135
|
+
)}
|
|
130
136
|
</div>
|
|
131
137
|
)
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
const TasksList: React.FC<{ setTaskId: (_: string) => void }> = ({ setTaskId }) => {
|
|
135
141
|
const allTodos = store.useQuery(allTodos$)
|
|
142
|
+
const handleTaskClick = React.useCallback(
|
|
143
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
144
|
+
const taskId = event.currentTarget.dataset.taskId
|
|
145
|
+
if (taskId !== undefined) {
|
|
146
|
+
setTaskId(taskId)
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
[setTaskId],
|
|
150
|
+
)
|
|
136
151
|
|
|
137
152
|
return (
|
|
138
153
|
<div>
|
|
139
154
|
{allTodos.map((_) => (
|
|
140
|
-
<div key={_.id}
|
|
155
|
+
<div key={_.id} data-task-id={_.id} onClick={handleTaskClick}>
|
|
141
156
|
{_.id}
|
|
142
157
|
</div>
|
|
143
158
|
))}
|
|
@@ -181,7 +196,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
181
196
|
}),
|
|
182
197
|
)
|
|
183
198
|
|
|
184
|
-
Vitest.
|
|
199
|
+
Vitest.live('should work for a useClientDocument query chained with a useTemporary query', () =>
|
|
185
200
|
Effect.gen(function* () {
|
|
186
201
|
const { store, wrapper, renderCount } = yield* makeTodoMvcReact({})
|
|
187
202
|
|
|
@@ -224,7 +239,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
224
239
|
}),
|
|
225
240
|
)
|
|
226
241
|
|
|
227
|
-
Vitest.
|
|
242
|
+
Vitest.live('kv client document overwrites value (Schema.Any, no partial merge)', () =>
|
|
228
243
|
Effect.gen(function* () {
|
|
229
244
|
const { wrapper, store, renderCount } = yield* makeTodoMvcReact({})
|
|
230
245
|
|
|
@@ -262,7 +277,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
262
277
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
263
278
|
})
|
|
264
279
|
|
|
265
|
-
const otelTracer = provider.getTracer(`testing-${strictMode ? 'strict' : 'non-strict'}`)
|
|
280
|
+
const otelTracer = provider.getTracer(`testing-${strictMode !== undefined ? 'strict' : 'non-strict'}`)
|
|
266
281
|
|
|
267
282
|
const span = otelTracer.startSpan('test-root')
|
|
268
283
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
@@ -312,7 +327,7 @@ Vitest.describe('useClientDocument', () => {
|
|
|
312
327
|
const stackInfo = JSON.parse(val as string) as LiveStore.StackInfo
|
|
313
328
|
// stackInfo.frames.shift() // Removes `renderHook.wrapper` from the stack
|
|
314
329
|
stackInfo.frames.forEach((_) => {
|
|
315
|
-
if (_.name.includes('renderHook.wrapper')) {
|
|
330
|
+
if (_.name.includes('renderHook.wrapper') === true) {
|
|
316
331
|
_.name = 'renderHook.wrapper'
|
|
317
332
|
}
|
|
318
333
|
_.filePath = '__REPLACED_FOR_SNAPSHOT__'
|
package/src/useClientDocument.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
1
3
|
import type { RowQuery } from '@livestore/common'
|
|
2
4
|
import { SessionIdSymbol } from '@livestore/common'
|
|
3
5
|
import { State } from '@livestore/common/schema'
|
|
6
|
+
import { removeUndefinedValues, type StateSetters, validateTableOptions } from '@livestore/framework-toolkit'
|
|
4
7
|
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
5
8
|
import { queryDb } from '@livestore/livestore'
|
|
6
9
|
import { omitUndefineds, shouldNeverHappen } from '@livestore/utils'
|
|
7
|
-
import React from 'react'
|
|
8
10
|
|
|
9
|
-
import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
10
11
|
import { useQueryRef } from './useQuery.ts'
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Return type of `useClientDocument` hook.
|
|
15
|
+
*
|
|
16
|
+
* A tuple providing React-style state access to a client-document table row:
|
|
17
|
+
* - `[0]` row: The current value (decoded according to the table schema)
|
|
18
|
+
* - `[1]` setRow: Setter function to update the document
|
|
19
|
+
* - `[2]` id: The document's ID (resolved from `SessionIdSymbol` if applicable)
|
|
20
|
+
* - `[3]` query$: The underlying `LiveQuery` for advanced use cases
|
|
21
|
+
*
|
|
22
|
+
* @typeParam TTableDef - The client-document table definition type
|
|
23
|
+
*/
|
|
12
24
|
export type UseClientDocumentResult<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = [
|
|
13
25
|
row: TTableDef['Value'],
|
|
14
26
|
setRow: StateSetters<TTableDef>,
|
|
@@ -104,20 +116,15 @@ export const useClientDocument: {
|
|
|
104
116
|
|
|
105
117
|
const tableName = table.sqliteDef.name
|
|
106
118
|
|
|
107
|
-
const store =
|
|
108
|
-
storeArg?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
109
|
-
React.useContext(LiveStoreContext)?.store ??
|
|
110
|
-
shouldNeverHappen(`No store provided to useClientDocument`)
|
|
119
|
+
const store = storeArg?.store ?? shouldNeverHappen(`No store provided to useClientDocument`)
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const idStr: string = id === SessionIdSymbol ? store.clientSession.sessionId : id
|
|
121
|
+
const idStr: string = id === SessionIdSymbol ? store.sessionId : id
|
|
115
122
|
|
|
116
123
|
type QueryDef = LiveQueryDef<TTableDef['Value']>
|
|
117
124
|
const queryDef: QueryDef = React.useMemo(
|
|
118
125
|
() =>
|
|
119
|
-
queryDb(table.get(id
|
|
120
|
-
deps: [idStr
|
|
126
|
+
queryDb(table.get(id, { default: defaultValues! }), {
|
|
127
|
+
deps: [idStr, table.sqliteDef.name, JSON.stringify(defaultValues)],
|
|
121
128
|
}),
|
|
122
129
|
[table, id, defaultValues, idStr],
|
|
123
130
|
)
|
|
@@ -132,36 +139,10 @@ export const useClientDocument: {
|
|
|
132
139
|
const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(queryRef.valueRef.current) : newValueOrFn
|
|
133
140
|
if (queryRef.valueRef.current === newValue) return
|
|
134
141
|
|
|
135
|
-
store.commit(table.set(removeUndefinedValues(newValue), id
|
|
142
|
+
store.commit(table.set(removeUndefinedValues(newValue), id))
|
|
136
143
|
},
|
|
137
144
|
[id, queryRef.valueRef, store, table],
|
|
138
145
|
)
|
|
139
146
|
|
|
140
147
|
return [queryRef.valueRef.current, setState, idStr, queryRef.queryRcRef.value]
|
|
141
148
|
}
|
|
142
|
-
|
|
143
|
-
export type Dispatch<A> = (action: A) => void
|
|
144
|
-
export type SetStateActionPartial<S> = Partial<S> | ((previousValue: S) => Partial<S>)
|
|
145
|
-
export type SetStateAction<S> = S | ((previousValue: S) => S)
|
|
146
|
-
|
|
147
|
-
export type StateSetters<TTableDef extends State.SQLite.ClientDocumentTableDef.TraitAny> = Dispatch<
|
|
148
|
-
TTableDef[State.SQLite.ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
|
149
|
-
? SetStateAction<TTableDef['Value']>
|
|
150
|
-
: SetStateActionPartial<TTableDef['Value']>
|
|
151
|
-
>
|
|
152
|
-
|
|
153
|
-
const validateTableOptions = (table: State.SQLite.TableDef<any, any>) => {
|
|
154
|
-
if (State.SQLite.tableIsClientDocumentTable(table) === false) {
|
|
155
|
-
return shouldNeverHappen(
|
|
156
|
-
`useClientDocument called on table "${table.sqliteDef.name}" which is not a client document table`,
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const removeUndefinedValues = (value: any) => {
|
|
162
|
-
if (typeof value === 'object' && value !== null) {
|
|
163
|
-
return Object.fromEntries(Object.entries(value).filter(([_, v]) => v !== undefined))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return value
|
|
167
|
-
}
|