@livestore/react 0.4.0-dev.2 → 0.4.0-dev.21
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 +27 -0
- package/dist/LiveStoreContext.d.ts.map +1 -1
- package/dist/LiveStoreContext.js +18 -0
- package/dist/LiveStoreContext.js.map +1 -1
- package/dist/LiveStoreProvider.d.ts +14 -8
- package/dist/LiveStoreProvider.d.ts.map +1 -1
- package/dist/LiveStoreProvider.js +40 -24
- package/dist/LiveStoreProvider.js.map +1 -1
- package/dist/LiveStoreProvider.test.js +7 -7
- package/dist/LiveStoreProvider.test.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +34 -12
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +13 -5
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.js +1 -1
- package/dist/experimental/mod.d.ts +1 -0
- package/dist/experimental/mod.d.ts.map +1 -1
- package/dist/experimental/mod.js +1 -0
- package/dist/experimental/mod.js.map +1 -1
- package/dist/experimental/multi-store/StoreRegistry.d.ts +105 -0
- package/dist/experimental/multi-store/StoreRegistry.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.js +184 -0
- package/dist/experimental/multi-store/StoreRegistry.js.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts +2 -0
- package/dist/experimental/multi-store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js +381 -0
- package/dist/experimental/multi-store/StoreRegistry.test.js.map +1 -0
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts +10 -0
- package/dist/experimental/multi-store/StoreRegistryContext.d.ts.map +1 -0
- package/dist/experimental/multi-store/StoreRegistryContext.js +15 -0
- package/dist/experimental/multi-store/StoreRegistryContext.js.map +1 -0
- package/dist/experimental/multi-store/mod.d.ts +6 -0
- package/dist/experimental/multi-store/mod.d.ts.map +1 -0
- package/dist/experimental/multi-store/mod.js +6 -0
- package/dist/experimental/multi-store/mod.js.map +1 -0
- package/dist/experimental/multi-store/storeOptions.d.ts +4 -0
- package/dist/experimental/multi-store/storeOptions.d.ts.map +1 -0
- package/dist/experimental/multi-store/storeOptions.js +4 -0
- package/dist/experimental/multi-store/storeOptions.js.map +1 -0
- package/dist/experimental/multi-store/types.d.ts +25 -0
- package/dist/experimental/multi-store/types.d.ts.map +1 -0
- package/dist/experimental/multi-store/types.js +2 -0
- package/dist/experimental/multi-store/types.js.map +1 -0
- package/dist/experimental/multi-store/useStore.d.ts +11 -0
- package/dist/experimental/multi-store/useStore.d.ts.map +1 -0
- package/dist/experimental/multi-store/useStore.js +16 -0
- package/dist/experimental/multi-store/useStore.js.map +1 -0
- package/dist/experimental/multi-store/useStore.test.d.ts +2 -0
- package/dist/experimental/multi-store/useStore.test.d.ts.map +1 -0
- package/dist/experimental/multi-store/useStore.test.js +198 -0
- package/dist/experimental/multi-store/useStore.test.js.map +1 -0
- package/dist/mod.d.ts +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +43 -13
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -5
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +29 -7
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +28 -6
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +63 -18
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +35 -11
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.test.js +1 -1
- package/dist/useStore.d.ts +53 -1
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +52 -1
- package/dist/useStore.js.map +1 -1
- package/package.json +14 -14
- package/src/LiveStoreContext.ts +27 -0
- package/src/LiveStoreProvider.test.tsx +7 -7
- package/src/LiveStoreProvider.tsx +67 -45
- package/src/__snapshots__/useClientDocument.test.tsx.snap +208 -100
- package/src/__snapshots__/useQuery.test.tsx.snap +400 -128
- package/src/__tests__/fixture.tsx +23 -24
- package/src/experimental/components/LiveList.tsx +1 -1
- package/src/experimental/mod.ts +1 -0
- package/src/experimental/multi-store/StoreRegistry.test.ts +518 -0
- package/src/experimental/multi-store/StoreRegistry.ts +253 -0
- package/src/experimental/multi-store/StoreRegistryContext.tsx +23 -0
- package/src/experimental/multi-store/mod.ts +5 -0
- package/src/experimental/multi-store/storeOptions.ts +8 -0
- package/src/experimental/multi-store/types.ts +37 -0
- package/src/experimental/multi-store/useStore.test.tsx +269 -0
- package/src/experimental/multi-store/useStore.ts +26 -0
- package/src/mod.ts +2 -1
- package/src/useClientDocument.test.tsx +105 -75
- package/src/useClientDocument.ts +58 -13
- package/src/useQuery.test.tsx +62 -11
- package/src/useQuery.ts +98 -27
- package/src/useRcResource.test.tsx +1 -1
- package/src/useStore.ts +55 -3
package/src/useQuery.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { isQueryBuilder } from '@livestore/common'
|
|
1
2
|
import type { LiveQuery, LiveQueryDef, Store } from '@livestore/livestore'
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
extractStackInfoFromStackTrace,
|
|
5
|
+
isQueryable,
|
|
6
|
+
type Queryable,
|
|
7
|
+
queryDb,
|
|
8
|
+
type SignalDef,
|
|
9
|
+
StoreInternalsSymbol,
|
|
10
|
+
stackInfoToString,
|
|
11
|
+
} from '@livestore/livestore'
|
|
3
12
|
import type { LiveQueries } from '@livestore/livestore/internal'
|
|
4
13
|
import { deepEqual, indent, shouldNeverHappen } from '@livestore/utils'
|
|
5
14
|
import * as otel from '@opentelemetry/api'
|
|
@@ -21,15 +30,36 @@ import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInp
|
|
|
21
30
|
* }
|
|
22
31
|
* ```
|
|
23
32
|
*/
|
|
24
|
-
export const useQuery = <
|
|
25
|
-
|
|
33
|
+
export const useQuery = <TQueryable extends Queryable<any>>(
|
|
34
|
+
queryable: TQueryable,
|
|
26
35
|
options?: { store?: Store },
|
|
27
|
-
):
|
|
36
|
+
): Queryable.Result<TQueryable> => useQueryRef(queryable, options).valueRef.current
|
|
28
37
|
|
|
29
38
|
/**
|
|
39
|
+
* Like `useQuery`, but also returns a reference to the underlying LiveQuery instance.
|
|
40
|
+
*
|
|
41
|
+
* Usage
|
|
42
|
+
* - Accepts any `Queryable<TResult>`: a `LiveQueryDef`, `SignalDef`, a `LiveQuery` instance
|
|
43
|
+
* or a SQL `QueryBuilder`. Unions of queryables are supported and the result type is
|
|
44
|
+
* inferred via `Queryable.Result<TQueryable>`.
|
|
45
|
+
* - Creates an OpenTelemetry span per unique query, reusing it while the ref-counted
|
|
46
|
+
* resource is alive. The span name is updated once the dynamic label is known.
|
|
47
|
+
* - Manages a reference-counted resource under-the-hood so query instances are shared
|
|
48
|
+
* across re-renders and properly disposed once no longer referenced.
|
|
49
|
+
*
|
|
50
|
+
* Parameters
|
|
51
|
+
* - `queryable`: The query definition/instance/builder to run and subscribe to.
|
|
52
|
+
* - `options.store`: Optional store to use; by default the store from `LiveStoreContext` is used.
|
|
53
|
+
* - `options.otelContext`: Optional parent otel context for the query span.
|
|
54
|
+
* - `options.otelSpanName`: Optional explicit span name; otherwise derived from the query label.
|
|
55
|
+
*
|
|
56
|
+
* Returns
|
|
57
|
+
* - `valueRef`: A React ref whose `current` holds the latest query result. The type is
|
|
58
|
+
* `Queryable.Result<TQueryable>` with full inference for unions.
|
|
59
|
+
* - `queryRcRef`: The underlying reference-counted `LiveQuery` instance used by the store.
|
|
30
60
|
*/
|
|
31
|
-
export const useQueryRef = <
|
|
32
|
-
|
|
61
|
+
export const useQueryRef = <TQueryable extends Queryable<any>>(
|
|
62
|
+
queryable: TQueryable,
|
|
33
63
|
options?: {
|
|
34
64
|
store?: Store
|
|
35
65
|
/** Parent otel context for the query */
|
|
@@ -38,17 +68,50 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
|
|
|
38
68
|
otelSpanName?: string
|
|
39
69
|
},
|
|
40
70
|
): {
|
|
41
|
-
valueRef: React.RefObject<
|
|
42
|
-
queryRcRef: LiveQueries.RcRef<LiveQuery<
|
|
71
|
+
valueRef: React.RefObject<Queryable.Result<TQueryable>>
|
|
72
|
+
queryRcRef: LiveQueries.RcRef<LiveQuery<Queryable.Result<TQueryable>>>
|
|
43
73
|
} => {
|
|
44
74
|
const store =
|
|
45
|
-
options?.store ??
|
|
46
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
75
|
+
options?.store ?? // biome-ignore lint/correctness/useHookAtTopLevel: store is stable
|
|
47
76
|
React.useContext(LiveStoreContext)?.store ??
|
|
48
77
|
shouldNeverHappen(`No store provided to useQuery`)
|
|
49
78
|
|
|
79
|
+
type TResult = Queryable.Result<TQueryable>
|
|
80
|
+
type NormalizedQueryable =
|
|
81
|
+
| { _tag: 'definition'; def: LiveQueryDef<TResult> | SignalDef<TResult> }
|
|
82
|
+
| { _tag: 'live-query'; query$: LiveQuery<TResult> }
|
|
83
|
+
|
|
84
|
+
const normalized = React.useMemo<NormalizedQueryable>(() => {
|
|
85
|
+
if (!isQueryable(queryable)) {
|
|
86
|
+
return shouldNeverHappen('useQuery expected a Queryable value')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isQueryBuilder(queryable)) {
|
|
90
|
+
return { _tag: 'definition', def: queryDb(queryable) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
(queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'def' ||
|
|
95
|
+
(queryable as LiveQueryDef<TResult> | SignalDef<TResult>)._tag === 'signal-def'
|
|
96
|
+
) {
|
|
97
|
+
return { _tag: 'definition', def: queryable as LiveQueryDef<TResult> | SignalDef<TResult> }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { _tag: 'live-query', query$: queryable as LiveQuery<TResult> }
|
|
101
|
+
}, [queryable])
|
|
102
|
+
|
|
50
103
|
// It's important to use all "aspects" of a store instance here, otherwise we get unexpected cache mappings
|
|
51
|
-
const rcRefKey =
|
|
104
|
+
const rcRefKey = React.useMemo(() => {
|
|
105
|
+
const base = `${store.storeId}_${store.clientId}_${store.sessionId}`
|
|
106
|
+
|
|
107
|
+
if (normalized._tag === 'definition') {
|
|
108
|
+
return `${base}:def:${normalized.def.hash}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return `${base}:instance:${normalized.query$.id}`
|
|
112
|
+
}, [normalized, store.clientId, store.sessionId, store.storeId])
|
|
113
|
+
|
|
114
|
+
const resourceLabel = normalized._tag === 'definition' ? normalized.def.label : normalized.query$.label
|
|
52
115
|
|
|
53
116
|
const stackInfo = React.useMemo(() => {
|
|
54
117
|
Error.stackTraceLimit = 10
|
|
@@ -60,17 +123,22 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
|
|
|
60
123
|
const { queryRcRef, span, otelContext } = useRcResource(
|
|
61
124
|
rcRefKey,
|
|
62
125
|
() => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
options?.
|
|
67
|
-
{ attributes: { label: queryDefLabel, firstStackInfo: JSON.stringify(stackInfo) } },
|
|
68
|
-
options?.otelContext ?? store.otel.queriesSpanContext,
|
|
126
|
+
const span = store[StoreInternalsSymbol].otel.tracer.startSpan(
|
|
127
|
+
options?.otelSpanName ?? `LiveStore:useQuery:${resourceLabel}`,
|
|
128
|
+
{ attributes: { label: resourceLabel, firstStackInfo: JSON.stringify(stackInfo) } },
|
|
129
|
+
options?.otelContext ?? store[StoreInternalsSymbol].otel.queriesSpanContext,
|
|
69
130
|
)
|
|
70
131
|
|
|
71
132
|
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
72
133
|
|
|
73
|
-
const queryRcRef =
|
|
134
|
+
const queryRcRef =
|
|
135
|
+
normalized._tag === 'definition'
|
|
136
|
+
? normalized.def.make(store[StoreInternalsSymbol].reactivityGraph.context!, otelContext)
|
|
137
|
+
: ({
|
|
138
|
+
value: normalized.query$,
|
|
139
|
+
deref: () => {},
|
|
140
|
+
rc: Number.POSITIVE_INFINITY,
|
|
141
|
+
} satisfies LiveQueries.RcRef<LiveQuery<TResult>>)
|
|
74
142
|
|
|
75
143
|
return { queryRcRef, span, otelContext }
|
|
76
144
|
},
|
|
@@ -83,7 +151,7 @@ export const useQueryRef = <TQuery extends LiveQueryDef.Any>(
|
|
|
83
151
|
// const queryRcRef.value.get()
|
|
84
152
|
// }
|
|
85
153
|
|
|
86
|
-
const query$ = queryRcRef.value as LiveQuery<
|
|
154
|
+
const query$ = queryRcRef.value as LiveQuery<TResult>
|
|
87
155
|
|
|
88
156
|
React.useDebugValue(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
89
157
|
// console.debug(`LiveStore:useQuery:${query$.id}:${query$.label}`)
|
|
@@ -119,7 +187,7 @@ Stack trace:
|
|
|
119
187
|
}, [otelContext, query$, stackInfo])
|
|
120
188
|
|
|
121
189
|
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
122
|
-
const [valueRef, setValue] = useStateRefWithReactiveInput<
|
|
190
|
+
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
123
191
|
|
|
124
192
|
// TODO we probably need to change the order of `useEffect` calls, so we destroy the query at the end
|
|
125
193
|
// before calling the LS `onEffect` on it
|
|
@@ -133,8 +201,9 @@ Stack trace:
|
|
|
133
201
|
// so we're also updating the span name here.
|
|
134
202
|
span.updateName(options?.otelSpanName ?? `LiveStore:useQuery:${query$.label}`)
|
|
135
203
|
|
|
136
|
-
return store.subscribe(
|
|
137
|
-
|
|
204
|
+
return store.subscribe(
|
|
205
|
+
query$,
|
|
206
|
+
(newValue) => {
|
|
138
207
|
// NOTE: we return a reference to the result object within LiveStore;
|
|
139
208
|
// this implies that app code must not mutate the results, or else
|
|
140
209
|
// there may be weird reactivity bugs.
|
|
@@ -142,12 +211,14 @@ Stack trace:
|
|
|
142
211
|
setValue(newValue)
|
|
143
212
|
}
|
|
144
213
|
},
|
|
145
|
-
|
|
146
|
-
|
|
214
|
+
{
|
|
215
|
+
onUnsubsubscribe: () => {
|
|
216
|
+
query$.activeSubscriptions.delete(stackInfo)
|
|
217
|
+
},
|
|
218
|
+
label: query$.label,
|
|
219
|
+
otelContext,
|
|
147
220
|
},
|
|
148
|
-
|
|
149
|
-
otelContext,
|
|
150
|
-
})
|
|
221
|
+
)
|
|
151
222
|
}, [stackInfo, query$, setValue, store, valueRef, otelContext, span, options?.otelSpanName])
|
|
152
223
|
|
|
153
224
|
useRcResource(
|
|
@@ -2,7 +2,7 @@ import * as ReactTesting from '@testing-library/react'
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.
|
|
5
|
+
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.ts'
|
|
6
6
|
|
|
7
7
|
describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (strictMode=%s)', ({ strictMode }) => {
|
|
8
8
|
beforeEach(() => {
|
package/src/useStore.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LiveStoreSchema } from '@livestore/common/schema'
|
|
1
2
|
import type { Store } from '@livestore/livestore'
|
|
2
3
|
import React from 'react'
|
|
3
4
|
|
|
@@ -6,16 +7,67 @@ import { LiveStoreContext } from './LiveStoreContext.ts'
|
|
|
6
7
|
import { useClientDocument } from './useClientDocument.ts'
|
|
7
8
|
import { useQuery } from './useQuery.ts'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Augments a Store instance with React-specific methods (`useQuery`, `useClientDocument`).
|
|
12
|
+
*
|
|
13
|
+
* This is called automatically by `useStore()` and `LiveStoreProvider`. You typically
|
|
14
|
+
* don't need to call it directly unless you're building custom integrations.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Usually not needed—useStore() does this automatically
|
|
19
|
+
* const store = withReactApi(myStore)
|
|
20
|
+
* const todos = store.useQuery(tables.todos.all())
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const withReactApi = <TSchema extends LiveStoreSchema>(store: Store<TSchema>): Store<TSchema> & ReactApi => {
|
|
10
24
|
// @ts-expect-error TODO properly implement this
|
|
11
25
|
|
|
12
|
-
store.useQuery = (
|
|
26
|
+
store.useQuery = (queryable) => useQuery(queryable, { store })
|
|
13
27
|
// @ts-expect-error TODO properly implement this
|
|
14
28
|
|
|
15
29
|
store.useClientDocument = (table, idOrOptions, options) => useClientDocument(table, idOrOptions, options, { store })
|
|
16
|
-
return store as Store & ReactApi
|
|
30
|
+
return store as Store<TSchema> & ReactApi
|
|
17
31
|
}
|
|
18
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Returns the current Store instance from React context, augmented with React-specific methods.
|
|
35
|
+
*
|
|
36
|
+
* Use this hook when you need direct access to the Store for operations like
|
|
37
|
+
* `store.commit()`, `store.subscribe()`, or accessing `store.sessionId`.
|
|
38
|
+
*
|
|
39
|
+
* For reactive queries, prefer `useQuery()` or `useClientDocument()` which handle
|
|
40
|
+
* subscriptions and re-renders automatically.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const MyComponent = () => {
|
|
45
|
+
* const { store } = useStore()
|
|
46
|
+
*
|
|
47
|
+
* const handleClick = () => {
|
|
48
|
+
* store.commit(events.todoCreated({ id: nanoid(), text: 'New todo' }))
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* return <button onClick={handleClick}>Add Todo</button>
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Access store metadata
|
|
58
|
+
* const { store } = useStore()
|
|
59
|
+
* console.log('Session ID:', store.sessionId)
|
|
60
|
+
* console.log('Client ID:', store.clientId)
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Use with an explicit store instance (bypasses context)
|
|
66
|
+
* const { store } = useStore({ store: myExternalStore })
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @throws Error if called outside of `<LiveStoreProvider>` or before the store is running
|
|
70
|
+
*/
|
|
19
71
|
export const useStore = (options?: { store?: Store }): { store: Store & ReactApi } => {
|
|
20
72
|
if (options?.store !== undefined) {
|
|
21
73
|
return { store: withReactApi(options.store) }
|