@veams/status-quo-query 0.12.0 → 0.13.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/.turbo/turbo-build.log +3 -3
- package/README.md +45 -15
- package/dist/mutation.d.ts +8 -8
- package/dist/mutation.js +9 -9
- package/dist/mutation.js.map +1 -1
- package/dist/provider.d.ts +2 -2
- package/dist/react/hooks/index.d.ts +1 -0
- package/dist/react/hooks/index.js +1 -0
- package/dist/react/hooks/index.js.map +1 -1
- package/dist/react/hooks/use-mutation-handle.d.ts +2 -0
- package/dist/react/hooks/use-mutation-handle.js +71 -0
- package/dist/react/hooks/use-mutation-handle.js.map +1 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/mutation.ts +27 -27
- package/src/provider.ts +7 -7
- package/src/react/__tests__/use-mutation-handle.spec.tsx +107 -0
- package/src/react/hooks/index.ts +1 -0
- package/src/react/hooks/use-mutation-handle.ts +98 -0
- package/src/react/index.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @veams/status-quo-query@0.
|
|
3
|
+
> @veams/status-quo-query@0.12.0 build
|
|
4
4
|
> npm-run-all compile
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
> @veams/status-quo-query@0.
|
|
7
|
+
> @veams/status-quo-query@0.12.0 compile
|
|
8
8
|
> npm-run-all bundle:ts
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
> @veams/status-quo-query@0.
|
|
11
|
+
> @veams/status-quo-query@0.12.0 bundle:ts
|
|
12
12
|
> tsc --project tsconfig.json
|
|
13
13
|
|
|
14
14
|
⠙[1G[0K⠙[1G[0K⠙[1G[0K
|
package/README.md
CHANGED
|
@@ -19,11 +19,11 @@ npm install react
|
|
|
19
19
|
Status Quo Query deliberately keeps the public surface small:
|
|
20
20
|
|
|
21
21
|
- `QueryHandle<TData, TError>` is the read handle for one query.
|
|
22
|
-
- `
|
|
22
|
+
- `MutationHandle<TData, TError, TVariables>` is the write handle for one mutation.
|
|
23
23
|
- snapshots are passive state objects returned from `getSnapshot()` and `subscribe(...)`.
|
|
24
24
|
- commands stay on the handle: `refetch()`, `invalidate()`, `mutate()`, `reset()`.
|
|
25
25
|
- `QueryManager` is the broader coordination layer for cross-query work.
|
|
26
|
-
- `@veams/status-quo-query/react` is optional and adds
|
|
26
|
+
- `@veams/status-quo-query/react` is optional and adds React subscription hooks (`useQueryHandle`, `useMutationHandle`) over the same handle shape.
|
|
27
27
|
|
|
28
28
|
That keeps the package usable in service code, query handlers, state handlers, and React components without changing the core query or mutation API.
|
|
29
29
|
|
|
@@ -49,13 +49,13 @@ Root exports:
|
|
|
49
49
|
- `CreateUntrackedMutation`
|
|
50
50
|
- `QueryHandle`
|
|
51
51
|
- `QueryHandleData`
|
|
52
|
-
- `
|
|
52
|
+
- `MutationHandle`
|
|
53
53
|
- `QueryHandleSnapshot`
|
|
54
|
-
- `
|
|
54
|
+
- `MutationHandleSnapshot`
|
|
55
55
|
- `QueryDependencyTuple`
|
|
56
56
|
- `QueryHandleOptions`
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
57
|
+
- `MutationHandleOptions`
|
|
58
|
+
- `TrackedMutationHandleOptions`
|
|
59
59
|
- `QueryInvalidateOptions`
|
|
60
60
|
- `QueryMetaState`
|
|
61
61
|
- `TrackedDependencyRecord`
|
|
@@ -70,7 +70,7 @@ Subpath exports:
|
|
|
70
70
|
- `@veams/status-quo-query/provider`
|
|
71
71
|
- `@veams/status-quo-query/query`
|
|
72
72
|
- `@veams/status-quo-query/mutation`
|
|
73
|
-
- `@veams/status-quo-query/react`
|
|
73
|
+
- `@veams/status-quo-query/react` (`useQueryHandle`, `useMutationHandle`)
|
|
74
74
|
|
|
75
75
|
## Quickstart
|
|
76
76
|
|
|
@@ -133,8 +133,9 @@ await userQuery.invalidate({ refetchType: 'none' });
|
|
|
133
133
|
|
|
134
134
|
## React Bindings
|
|
135
135
|
|
|
136
|
-
The React entrypoint exposes `useQueryHandle(...)` and keeps `react` optional unless you
|
|
137
|
-
|
|
136
|
+
The React entrypoint exposes `useQueryHandle(...)` and `useMutationHandle(...)` and keeps `react` optional unless you import `@veams/status-quo-query/react`.
|
|
137
|
+
|
|
138
|
+
### `useQueryHandle`
|
|
138
139
|
|
|
139
140
|
```tsx
|
|
140
141
|
import { useQueryHandle } from '@veams/status-quo-query/react';
|
|
@@ -153,6 +154,35 @@ Use the hook when a component should subscribe directly to a query handle and re
|
|
|
153
154
|
- call `query.refetch()` or `query.invalidate()` on the handle itself
|
|
154
155
|
- derive view-specific values in the component instead of adding selector logic to the hook
|
|
155
156
|
|
|
157
|
+
### `useMutationHandle`
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { useMutationHandle } from '@veams/status-quo-query/react';
|
|
161
|
+
import type { MutationHandle } from '@veams/status-quo-query';
|
|
162
|
+
|
|
163
|
+
function SaveButton({
|
|
164
|
+
mutation,
|
|
165
|
+
payload,
|
|
166
|
+
}: {
|
|
167
|
+
mutation: MutationHandle<{ ok: boolean }, Error, { name: string }>;
|
|
168
|
+
payload: { name: string };
|
|
169
|
+
}) {
|
|
170
|
+
const snapshot = useMutationHandle(mutation);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<button onClick={() => mutation.mutate(payload)} disabled={snapshot.isPending}>
|
|
174
|
+
{snapshot.isPending ? 'Saving…' : 'Save'}
|
|
175
|
+
</button>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Use `useMutationHandle` when a component should react to mutation state (pending, success, error). Keep imperative calls on the handle itself:
|
|
181
|
+
|
|
182
|
+
- read `status`, `isPending`, `isError`, `isSuccess`, `data`, `error`, `variables` from the snapshot
|
|
183
|
+
- call `mutation.mutate(variables)` or `mutation.reset()` on the handle itself
|
|
184
|
+
- the hook does not trigger the mutation — it only subscribes to its state
|
|
185
|
+
|
|
156
186
|
## Status Quo Integration
|
|
157
187
|
|
|
158
188
|
The same query handle can also feed a `status-quo` handler through `bindSubscribable(...)`.
|
|
@@ -763,7 +793,7 @@ Only `deps` participates in automatic invalidation tracking. `view` is optional
|
|
|
763
793
|
|
|
764
794
|
`createQuery(queryKey, queryFn, options?)` returns the same `QueryHandle<TData, TError>` shape as `createUntrackedQuery(...)`, but it registers the query hash under every `deps` entry, re-registers on `refetch()` or the first `subscribe(...)` if TanStack has removed the cache entry in the meantime, and keeps the registry in sync when `dependsOn` derives a new tracked key at runtime.
|
|
765
795
|
|
|
766
|
-
`createMutation(mutationFn, options?)` returns the same `
|
|
796
|
+
`createMutation(mutationFn, options?)` returns the same `MutationHandle<TData, TError, TVariables, TOnMutateResult>` shape as `createUntrackedMutation(...)`, but adds:
|
|
767
797
|
|
|
768
798
|
- `dependencyKeys?`
|
|
769
799
|
- `resolveDependencies?`
|
|
@@ -786,7 +816,7 @@ Captures dependency names once and returns:
|
|
|
786
816
|
- the tracked query factory
|
|
787
817
|
- a tracked mutation factory whose default resolver reads `variables[dependencyKey]`
|
|
788
818
|
|
|
789
|
-
The tracked query factory still expects a query key with a final `{ deps, view? }` segment. The tracked mutation factory keeps the same `
|
|
819
|
+
The tracked query factory still expects a query key with a final `{ deps, view? }` segment. The tracked mutation factory keeps the same `MutationHandle` shape as `createMutation(...)`, but no longer needs `dependencyKeys` repeated in each call.
|
|
790
820
|
|
|
791
821
|
Reach for standalone `createMutation(...)` when:
|
|
792
822
|
|
|
@@ -844,11 +874,11 @@ It also adds:
|
|
|
844
874
|
|
|
845
875
|
Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
|
|
846
876
|
|
|
847
|
-
`createUntrackedMutation(mutationFn, options?)` returns `
|
|
877
|
+
`createUntrackedMutation(mutationFn, options?)` returns `MutationHandle<TData, TError, TVariables, TOnMutateResult>`.
|
|
848
878
|
|
|
849
|
-
`
|
|
879
|
+
`MutationHandleOptions` is based on TanStack `MutationObserverOptions`, without `mutationFn` because it is provided directly to `createUntrackedMutation`.
|
|
850
880
|
|
|
851
|
-
`
|
|
881
|
+
`MutationHandle` methods:
|
|
852
882
|
|
|
853
883
|
- `getSnapshot()`
|
|
854
884
|
- `subscribe(listener)`
|
|
@@ -856,7 +886,7 @@ Creates a `createUntrackedMutation` factory bound to a `QueryClient`.
|
|
|
856
886
|
- `reset()`
|
|
857
887
|
- `unsafe_getResult()`
|
|
858
888
|
|
|
859
|
-
`
|
|
889
|
+
`MutationHandleSnapshot<TData, TError, TVariables>` fields:
|
|
860
890
|
|
|
861
891
|
- `data`
|
|
862
892
|
- `error`
|
package/dist/mutation.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type MutationStatus = TanstackMutationStatus;
|
|
|
4
4
|
/**
|
|
5
5
|
* Represents a stable snapshot of the mutation service's state.
|
|
6
6
|
*/
|
|
7
|
-
export interface
|
|
7
|
+
export interface MutationHandleSnapshot<TData = unknown, TError = Error, TVariables = void> {
|
|
8
8
|
data: TData | undefined;
|
|
9
9
|
error: TError | null;
|
|
10
10
|
status: MutationStatus;
|
|
@@ -17,9 +17,9 @@ export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVaria
|
|
|
17
17
|
/**
|
|
18
18
|
* Defines the public API for a mutation service.
|
|
19
19
|
*/
|
|
20
|
-
export interface
|
|
21
|
-
getSnapshot: () =>
|
|
22
|
-
subscribe: (listener: (snapshot:
|
|
20
|
+
export interface MutationHandle<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> {
|
|
21
|
+
getSnapshot: () => MutationHandleSnapshot<TData, TError, TVariables>;
|
|
22
|
+
subscribe: (listener: (snapshot: MutationHandleSnapshot<TData, TError, TVariables>) => void) => () => void;
|
|
23
23
|
mutate: (variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TOnMutateResult>) => Promise<TData>;
|
|
24
24
|
reset: () => void;
|
|
25
25
|
unsafe_getResult: () => MutationObserverResult<TData, TError, TVariables, TOnMutateResult>;
|
|
@@ -27,12 +27,12 @@ export interface MutationService<TData = unknown, TError = Error, TVariables = v
|
|
|
27
27
|
/**
|
|
28
28
|
* Configuration options for creating a mutation service, excluding the mutation function itself.
|
|
29
29
|
*/
|
|
30
|
-
export type
|
|
30
|
+
export type MutationHandleOptions<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> = Omit<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, 'mutationFn'>;
|
|
31
31
|
/**
|
|
32
32
|
* Function signature for the untracked mutation factory.
|
|
33
33
|
*/
|
|
34
34
|
export interface CreateUntrackedMutation {
|
|
35
|
-
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?:
|
|
35
|
+
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
38
|
* Additional options for tracked mutations that invalidate queries automatically.
|
|
@@ -41,7 +41,7 @@ export interface CreateUntrackedMutation {
|
|
|
41
41
|
* options only describe how the facade should derive dependency values and when it should
|
|
42
42
|
* invalidate matching tracked queries after the mutation lifecycle settles.
|
|
43
43
|
*/
|
|
44
|
-
export interface
|
|
44
|
+
export interface TrackedMutationHandleOptions<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown> extends MutationHandleOptions<TData, TError, TVariables, TOnMutateResult> {
|
|
45
45
|
dependencyKeys?: readonly (keyof TDeps & string)[];
|
|
46
46
|
resolveDependencies?: (variables: TVariables) => Partial<TDeps>;
|
|
47
47
|
invalidateOn?: TrackedInvalidateOn;
|
|
@@ -51,7 +51,7 @@ export interface TrackedMutationServiceOptions<TDeps extends TrackedDependencyRe
|
|
|
51
51
|
* Function signature for the default mutation factory with automatic invalidation.
|
|
52
52
|
*/
|
|
53
53
|
export interface CreateMutation {
|
|
54
|
-
<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?:
|
|
54
|
+
<TDeps extends TrackedDependencyRecord = TrackedDependencyRecord, TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Prepares the mutation factory by binding it to a specific QueryClient instance.
|
package/dist/mutation.js
CHANGED
|
@@ -8,7 +8,7 @@ import { pickTrackedDependencies, resolveTrackedQueries, toTrackedDependencyEntr
|
|
|
8
8
|
export function setupMutation(queryClient) {
|
|
9
9
|
// Returns the actual factory function for creating individual mutation services.
|
|
10
10
|
return function createMutation(mutationFn, options) {
|
|
11
|
-
return
|
|
11
|
+
return createMutationHandle(queryClient, mutationFn, options);
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
@@ -22,8 +22,8 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
|
|
|
22
22
|
return function createMutation(mutationFn, options) {
|
|
23
23
|
// Split tracked-only options from the underlying TanStack mutation observer options.
|
|
24
24
|
const { dependencyKeys, invalidateOn = 'success', matchMode = 'intersection', resolveDependencies, ...mutationOptions } = options ?? {};
|
|
25
|
-
// Reuse the normal mutation
|
|
26
|
-
const
|
|
25
|
+
// Reuse the normal mutation handle so snapshots and subscription behavior stay identical.
|
|
26
|
+
const handle = createMutationHandle(queryClient, mutationFn, mutationOptions);
|
|
27
27
|
// The paired helper injects dependency keys here, while standalone tracked mutations can
|
|
28
28
|
// still provide them directly or bypass them with a custom resolver.
|
|
29
29
|
const resolvedDependencyKeys = (dependencyKeys ?? defaultDependencyKeys);
|
|
@@ -41,12 +41,12 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
|
|
|
41
41
|
})));
|
|
42
42
|
};
|
|
43
43
|
return {
|
|
44
|
-
...
|
|
44
|
+
...handle,
|
|
45
45
|
mutate: async (variables, mutateOptions) => {
|
|
46
46
|
try {
|
|
47
47
|
// Let TanStack finish the mutation first so its own callbacks and state machine remain
|
|
48
48
|
// authoritative. The facade only coordinates the follow-up invalidation.
|
|
49
|
-
const result = await
|
|
49
|
+
const result = await handle.mutate(variables, mutateOptions);
|
|
50
50
|
if (invalidateOn === 'success' || invalidateOn === 'settled') {
|
|
51
51
|
await invalidateTrackedQueries(variables);
|
|
52
52
|
}
|
|
@@ -71,7 +71,7 @@ export function setupTrackedMutation(queryClient, trackingRegistry, defaultDepen
|
|
|
71
71
|
/**
|
|
72
72
|
* Internal helper to transform a raw Tanstack mutation result into our public snapshot format.
|
|
73
73
|
*/
|
|
74
|
-
function
|
|
74
|
+
function toMutationHandleSnapshot(result) {
|
|
75
75
|
// Extract and return the relevant fields for the UI or other services.
|
|
76
76
|
return {
|
|
77
77
|
data: result.data,
|
|
@@ -84,7 +84,7 @@ function toMutationServiceSnapshot(result) {
|
|
|
84
84
|
isSuccess: result.isSuccess,
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
function
|
|
87
|
+
function createMutationHandle(queryClient, mutationFn, options) {
|
|
88
88
|
// Keep the original mutation implementation in one place so tracked and untracked mutations
|
|
89
89
|
// always expose the same observer-backed runtime behavior.
|
|
90
90
|
const observer = new MutationObserver(queryClient, {
|
|
@@ -92,9 +92,9 @@ function createMutationService(queryClient, mutationFn, options) {
|
|
|
92
92
|
mutationFn,
|
|
93
93
|
});
|
|
94
94
|
return {
|
|
95
|
-
getSnapshot: () =>
|
|
95
|
+
getSnapshot: () => toMutationHandleSnapshot(observer.getCurrentResult()),
|
|
96
96
|
subscribe: (listener) => observer.subscribe((result) => {
|
|
97
|
-
listener(
|
|
97
|
+
listener(toMutationHandleSnapshot(result));
|
|
98
98
|
}),
|
|
99
99
|
mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
|
|
100
100
|
reset: () => observer.reset(),
|
package/dist/mutation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"AAAA,OAAO;AACL,2DAA2D;AAC3D,gBAAgB,GAajB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAML,uBAAuB,EACvB,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,eAAe,CAAC;AAmHvB;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,WAAwB;IACpD,iFAAiF;IACjF,OAAO,SAAS,cAAc,CAM5B,UAA+C,EAC/C,
|
|
1
|
+
{"version":3,"file":"mutation.js","sourceRoot":"","sources":["../src/mutation.ts"],"names":[],"mappings":"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"}
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type QueryClient, type MutationFunction } from '@tanstack/query-core';
|
|
2
|
-
import { type CreateMutation, type CreateUntrackedMutation, type
|
|
2
|
+
import { type CreateMutation, type CreateUntrackedMutation, type MutationHandle, type TrackedMutationHandleOptions } from './mutation.js';
|
|
3
3
|
import { type CreateQuery, type CreateUntrackedQuery } from './query.js';
|
|
4
4
|
import { type TrackedDependencyValue } from './tracking.js';
|
|
5
5
|
/**
|
|
@@ -9,7 +9,7 @@ import { type TrackedDependencyValue } from './tracking.js';
|
|
|
9
9
|
* once at setup time and injects them automatically for each tracked mutation it creates.
|
|
10
10
|
*/
|
|
11
11
|
export interface CreateMutationWithDefaults<TDependencyKey extends string> {
|
|
12
|
-
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: Omit<
|
|
12
|
+
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: Omit<TrackedMutationHandleOptions<Record<TDependencyKey, TrackedDependencyValue>, TData, TError, TVariables, TOnMutateResult>, 'dependencyKeys'>): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Paired tracked helper that captures dependency keys once for default mutation resolution.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Import the React hooks needed to memoize callbacks, hold mutable cache state,
|
|
2
|
+
// and connect an external store to React rendering.
|
|
3
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
// Subscribe a React component to a MutationHandle and return its latest snapshot.
|
|
5
|
+
export function useMutationHandle(
|
|
6
|
+
// Receive the external mutation handle instance to subscribe to.
|
|
7
|
+
mutationHandle) {
|
|
8
|
+
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
9
|
+
const snapshotVersionRef = useRef(0);
|
|
10
|
+
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
11
|
+
const snapshotCacheRef = useRef(null);
|
|
12
|
+
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
13
|
+
const serverSnapshotCacheRef = useRef(null);
|
|
14
|
+
// Create the subscribe function expected by useSyncExternalStore.
|
|
15
|
+
const subscribe = useCallback(
|
|
16
|
+
// React passes a listener that must run whenever the external store changes.
|
|
17
|
+
(listener) =>
|
|
18
|
+
// Forward the subscription to the mutation handle.
|
|
19
|
+
mutationHandle.subscribe(() => {
|
|
20
|
+
// Bump the version so later reads know the previous cache is outdated.
|
|
21
|
+
snapshotVersionRef.current += 1;
|
|
22
|
+
// Drop the cached client snapshot because the store just changed.
|
|
23
|
+
snapshotCacheRef.current = null;
|
|
24
|
+
// Drop the cached server snapshot for the same reason.
|
|
25
|
+
serverSnapshotCacheRef.current = null;
|
|
26
|
+
// Notify React that it should read a fresh snapshot.
|
|
27
|
+
listener();
|
|
28
|
+
}),
|
|
29
|
+
// Recreate the subscription function only when the handle instance changes.
|
|
30
|
+
[mutationHandle]);
|
|
31
|
+
// Read the current client snapshot in a referentially stable way for React.
|
|
32
|
+
const getSnapshot = useCallback(() => {
|
|
33
|
+
// Read the latest store version number.
|
|
34
|
+
const version = snapshotVersionRef.current;
|
|
35
|
+
// Read the last cached client snapshot, if there is one.
|
|
36
|
+
const cachedSnapshot = snapshotCacheRef.current;
|
|
37
|
+
// Reuse the cached snapshot when it was produced for the current version.
|
|
38
|
+
if (cachedSnapshot && cachedSnapshot.version === version) {
|
|
39
|
+
// Return the cached snapshot so repeated reads in the same render stay stable.
|
|
40
|
+
return cachedSnapshot.snapshot;
|
|
41
|
+
}
|
|
42
|
+
// Ask the mutation handle for the latest snapshot because the cache is empty or stale.
|
|
43
|
+
const snapshot = mutationHandle.getSnapshot();
|
|
44
|
+
// Store the new snapshot together with the version it belongs to.
|
|
45
|
+
snapshotCacheRef.current = {
|
|
46
|
+
snapshot,
|
|
47
|
+
version,
|
|
48
|
+
};
|
|
49
|
+
// Return the freshly read snapshot to React.
|
|
50
|
+
return snapshot;
|
|
51
|
+
}, [mutationHandle]);
|
|
52
|
+
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
53
|
+
const getServerSnapshot = useCallback(() => {
|
|
54
|
+
// Read the cached server snapshot, if one was stored earlier.
|
|
55
|
+
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
56
|
+
// Reuse the cached server snapshot to keep server reads stable.
|
|
57
|
+
if (cachedSnapshot) {
|
|
58
|
+
// Return the cached server snapshot directly.
|
|
59
|
+
return cachedSnapshot;
|
|
60
|
+
}
|
|
61
|
+
// Ask the mutation handle for a snapshot because no server cache exists yet.
|
|
62
|
+
const snapshot = mutationHandle.getSnapshot();
|
|
63
|
+
// Cache that snapshot for the next server read.
|
|
64
|
+
serverSnapshotCacheRef.current = snapshot;
|
|
65
|
+
// Return the freshly read server snapshot.
|
|
66
|
+
return snapshot;
|
|
67
|
+
}, [mutationHandle]);
|
|
68
|
+
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
69
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=use-mutation-handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-mutation-handle.js","sourceRoot":"","sources":["../../../src/react/hooks/use-mutation-handle.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,oDAAoD;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAelE,kFAAkF;AAClF,MAAM,UAAU,iBAAiB;AAC/B,iEAAiE;AACjE,cAAyD;IAEzD,8EAA8E;IAC9E,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrC,kFAAkF;IAClF,MAAM,gBAAgB,GAAG,MAAM,CAAuD,IAAI,CAAC,CAAC;IAC5F,kFAAkF;IAClF,MAAM,sBAAsB,GAAG,MAAM,CACnC,IAAI,CACL,CAAC;IAEF,kEAAkE;IAClE,MAAM,SAAS,GAAG,WAAW;IAC3B,6EAA6E;IAC7E,CAAC,QAAkB,EAAE,EAAE;IACrB,mDAAmD;IACnD,cAAc,CAAC,SAAS,CAAC,GAAG,EAAE;QAC5B,uEAAuE;QACvE,kBAAkB,CAAC,OAAO,IAAI,CAAC,CAAC;QAChC,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAC;QAChC,uDAAuD;QACvD,sBAAsB,CAAC,OAAO,GAAG,IAAI,CAAC;QACtC,qDAAqD;QACrD,QAAQ,EAAE,CAAC;IACb,CAAC,CAAC;IACJ,4EAA4E;IAC5E,CAAC,cAAc,CAAC,CACjB,CAAC;IAEF,4EAA4E;IAC5E,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,wCAAwC;QACxC,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC;QAC3C,yDAAyD;QACzD,MAAM,cAAc,GAAG,gBAAgB,CAAC,OAAO,CAAC;QAEhD,0EAA0E;QAC1E,IAAI,cAAc,IAAI,cAAc,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACzD,+EAA+E;YAC/E,OAAO,cAAc,CAAC,QAAQ,CAAC;QACjC,CAAC;QAED,uFAAuF;QACvF,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,EAAE,CAAC;QAC9C,kEAAkE;QAClE,gBAAgB,CAAC,OAAO,GAAG;YACzB,QAAQ;YACR,OAAO;SACR,CAAC;QAEF,6CAA6C;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAErB,iFAAiF;IACjF,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,EAAE;QACzC,8DAA8D;QAC9D,MAAM,cAAc,GAAG,sBAAsB,CAAC,OAAO,CAAC;QAEtD,gEAAgE;QAChE,IAAI,cAAc,EAAE,CAAC;YACnB,8CAA8C;YAC9C,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,EAAE,CAAC;QAC9C,gDAAgD;QAChD,sBAAsB,CAAC,OAAO,GAAG,QAAQ,CAAC;QAE1C,2CAA2C;QAC3C,OAAO,QAAQ,CAAC;IAClB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC;IAErB,4FAA4F;IAC5F,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { useQueryHandle } from './hooks/index.js';
|
|
1
|
+
export { useQueryHandle, useMutationHandle } from './hooks/index.js';
|
package/dist/react/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { useQueryHandle } from './hooks/index.js';
|
|
1
|
+
export { useQueryHandle, useMutationHandle } from './hooks/index.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/package.json
CHANGED
package/src/mutation.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type MutationStatus = TanstackMutationStatus;
|
|
|
32
32
|
/**
|
|
33
33
|
* Represents a stable snapshot of the mutation service's state.
|
|
34
34
|
*/
|
|
35
|
-
export interface
|
|
35
|
+
export interface MutationHandleSnapshot<TData = unknown, TError = Error, TVariables = void> {
|
|
36
36
|
// The data returned from a successful mutation.
|
|
37
37
|
data: TData | undefined;
|
|
38
38
|
// The error object if the mutation failed.
|
|
@@ -54,17 +54,17 @@ export interface MutationServiceSnapshot<TData = unknown, TError = Error, TVaria
|
|
|
54
54
|
/**
|
|
55
55
|
* Defines the public API for a mutation service.
|
|
56
56
|
*/
|
|
57
|
-
export interface
|
|
57
|
+
export interface MutationHandle<
|
|
58
58
|
TData = unknown,
|
|
59
59
|
TError = Error,
|
|
60
60
|
TVariables = void,
|
|
61
61
|
TOnMutateResult = unknown,
|
|
62
62
|
> {
|
|
63
63
|
// Returns the current state snapshot of the mutation.
|
|
64
|
-
getSnapshot: () =>
|
|
64
|
+
getSnapshot: () => MutationHandleSnapshot<TData, TError, TVariables>;
|
|
65
65
|
// Subscribes a listener to state changes; returns an unsubscribe function.
|
|
66
66
|
subscribe: (
|
|
67
|
-
listener: (snapshot:
|
|
67
|
+
listener: (snapshot: MutationHandleSnapshot<TData, TError, TVariables>) => void
|
|
68
68
|
) => () => void;
|
|
69
69
|
// Triggers the mutation with the given variables and optional lifecycle callbacks.
|
|
70
70
|
mutate: (
|
|
@@ -80,7 +80,7 @@ export interface MutationService<
|
|
|
80
80
|
/**
|
|
81
81
|
* Configuration options for creating a mutation service, excluding the mutation function itself.
|
|
82
82
|
*/
|
|
83
|
-
export type
|
|
83
|
+
export type MutationHandleOptions<
|
|
84
84
|
TData = unknown,
|
|
85
85
|
TError = Error,
|
|
86
86
|
TVariables = void,
|
|
@@ -95,8 +95,8 @@ export interface CreateUntrackedMutation {
|
|
|
95
95
|
// The asynchronous function that performs the mutation.
|
|
96
96
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
97
97
|
// Optional configuration for behavior like retry or lifecycle hooks.
|
|
98
|
-
options?:
|
|
99
|
-
):
|
|
98
|
+
options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
|
|
99
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
@@ -106,13 +106,13 @@ export interface CreateUntrackedMutation {
|
|
|
106
106
|
* options only describe how the facade should derive dependency values and when it should
|
|
107
107
|
* invalidate matching tracked queries after the mutation lifecycle settles.
|
|
108
108
|
*/
|
|
109
|
-
export interface
|
|
109
|
+
export interface TrackedMutationHandleOptions<
|
|
110
110
|
TDeps extends TrackedDependencyRecord = TrackedDependencyRecord,
|
|
111
111
|
TData = unknown,
|
|
112
112
|
TError = Error,
|
|
113
113
|
TVariables = void,
|
|
114
114
|
TOnMutateResult = unknown,
|
|
115
|
-
> extends
|
|
115
|
+
> extends MutationHandleOptions<TData, TError, TVariables, TOnMutateResult> {
|
|
116
116
|
// Optional dependency keys used by the default variable reader.
|
|
117
117
|
dependencyKeys?: readonly (keyof TDeps & string)[];
|
|
118
118
|
// Optional custom resolver when mutation variables do not expose dependency fields directly.
|
|
@@ -135,8 +135,8 @@ export interface CreateMutation {
|
|
|
135
135
|
TOnMutateResult = unknown,
|
|
136
136
|
>(
|
|
137
137
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
138
|
-
options?:
|
|
139
|
-
):
|
|
138
|
+
options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
|
|
139
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
@@ -151,9 +151,9 @@ export function setupMutation(queryClient: QueryClient): CreateUntrackedMutation
|
|
|
151
151
|
TOnMutateResult = unknown,
|
|
152
152
|
>(
|
|
153
153
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
154
|
-
options?:
|
|
155
|
-
):
|
|
156
|
-
return
|
|
154
|
+
options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
|
|
155
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
|
|
156
|
+
return createMutationHandle(queryClient, mutationFn, options);
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -177,8 +177,8 @@ export function setupTrackedMutation(
|
|
|
177
177
|
TOnMutateResult = unknown,
|
|
178
178
|
>(
|
|
179
179
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
180
|
-
options?:
|
|
181
|
-
):
|
|
180
|
+
options?: TrackedMutationHandleOptions<TDeps, TData, TError, TVariables, TOnMutateResult>
|
|
181
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
|
|
182
182
|
// Split tracked-only options from the underlying TanStack mutation observer options.
|
|
183
183
|
const {
|
|
184
184
|
dependencyKeys,
|
|
@@ -187,8 +187,8 @@ export function setupTrackedMutation(
|
|
|
187
187
|
resolveDependencies,
|
|
188
188
|
...mutationOptions
|
|
189
189
|
} = options ?? {};
|
|
190
|
-
// Reuse the normal mutation
|
|
191
|
-
const
|
|
190
|
+
// Reuse the normal mutation handle so snapshots and subscription behavior stay identical.
|
|
191
|
+
const handle = createMutationHandle(queryClient, mutationFn, mutationOptions);
|
|
192
192
|
// The paired helper injects dependency keys here, while standalone tracked mutations can
|
|
193
193
|
// still provide them directly or bypass them with a custom resolver.
|
|
194
194
|
const resolvedDependencyKeys = (dependencyKeys ?? defaultDependencyKeys);
|
|
@@ -220,12 +220,12 @@ export function setupTrackedMutation(
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
return {
|
|
223
|
-
...
|
|
223
|
+
...handle,
|
|
224
224
|
mutate: async (variables, mutateOptions) => {
|
|
225
225
|
try {
|
|
226
226
|
// Let TanStack finish the mutation first so its own callbacks and state machine remain
|
|
227
227
|
// authoritative. The facade only coordinates the follow-up invalidation.
|
|
228
|
-
const result = await
|
|
228
|
+
const result = await handle.mutate(variables, mutateOptions);
|
|
229
229
|
|
|
230
230
|
if (invalidateOn === 'success' || invalidateOn === 'settled') {
|
|
231
231
|
await invalidateTrackedQueries(variables);
|
|
@@ -252,9 +252,9 @@ export function setupTrackedMutation(
|
|
|
252
252
|
/**
|
|
253
253
|
* Internal helper to transform a raw Tanstack mutation result into our public snapshot format.
|
|
254
254
|
*/
|
|
255
|
-
function
|
|
255
|
+
function toMutationHandleSnapshot<TData, TError, TVariables, TOnMutateResult>(
|
|
256
256
|
result: MutationObserverResult<TData, TError, TVariables, TOnMutateResult>
|
|
257
|
-
):
|
|
257
|
+
): MutationHandleSnapshot<TData, TError, TVariables> {
|
|
258
258
|
// Extract and return the relevant fields for the UI or other services.
|
|
259
259
|
return {
|
|
260
260
|
data: result.data,
|
|
@@ -268,7 +268,7 @@ function toMutationServiceSnapshot<TData, TError, TVariables, TOnMutateResult>(
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function
|
|
271
|
+
function createMutationHandle<
|
|
272
272
|
TData = unknown,
|
|
273
273
|
TError = Error,
|
|
274
274
|
TVariables = void,
|
|
@@ -276,8 +276,8 @@ function createMutationService<
|
|
|
276
276
|
>(
|
|
277
277
|
queryClient: QueryClient,
|
|
278
278
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
279
|
-
options?:
|
|
280
|
-
):
|
|
279
|
+
options?: MutationHandleOptions<TData, TError, TVariables, TOnMutateResult>
|
|
280
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult> {
|
|
281
281
|
// Keep the original mutation implementation in one place so tracked and untracked mutations
|
|
282
282
|
// always expose the same observer-backed runtime behavior.
|
|
283
283
|
const observer = new MutationObserver<TData, TError, TVariables, TOnMutateResult>(queryClient, {
|
|
@@ -286,10 +286,10 @@ function createMutationService<
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
return {
|
|
289
|
-
getSnapshot: () =>
|
|
289
|
+
getSnapshot: () => toMutationHandleSnapshot(observer.getCurrentResult()),
|
|
290
290
|
subscribe: (listener) =>
|
|
291
291
|
observer.subscribe((result) => {
|
|
292
|
-
listener(
|
|
292
|
+
listener(toMutationHandleSnapshot(result));
|
|
293
293
|
}),
|
|
294
294
|
mutate: (variables, mutateOptions) => observer.mutate(variables, mutateOptions),
|
|
295
295
|
reset: () => observer.reset(),
|
package/src/provider.ts
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
type CreateMutation,
|
|
10
10
|
type CreateUntrackedMutation,
|
|
11
|
-
type
|
|
12
|
-
type
|
|
11
|
+
type MutationHandle,
|
|
12
|
+
type TrackedMutationHandleOptions,
|
|
13
13
|
setupMutation,
|
|
14
14
|
setupTrackedMutation,
|
|
15
15
|
} from './mutation.js';
|
|
@@ -29,7 +29,7 @@ export interface CreateMutationWithDefaults<TDependencyKey extends string> {
|
|
|
29
29
|
<TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
|
|
30
30
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
31
31
|
options?: Omit<
|
|
32
|
-
|
|
32
|
+
TrackedMutationHandleOptions<
|
|
33
33
|
Record<TDependencyKey, TrackedDependencyValue>,
|
|
34
34
|
TData,
|
|
35
35
|
TError,
|
|
@@ -38,7 +38,7 @@ export interface CreateMutationWithDefaults<TDependencyKey extends string> {
|
|
|
38
38
|
>,
|
|
39
39
|
'dependencyKeys'
|
|
40
40
|
>
|
|
41
|
-
):
|
|
41
|
+
): MutationHandle<TData, TError, TVariables, TOnMutateResult>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -54,13 +54,13 @@ export interface CreateQueryAndMutation {
|
|
|
54
54
|
* Defines the public API for the query manager facade.
|
|
55
55
|
*/
|
|
56
56
|
export interface QueryManager {
|
|
57
|
-
// Factory for creating a dependency-tracked mutation
|
|
57
|
+
// Factory for creating a dependency-tracked mutation handle within the context of this provider.
|
|
58
58
|
createMutation: CreateMutation;
|
|
59
59
|
// Factory for creating a dependency-tracked query handle within the context of this provider.
|
|
60
60
|
createQuery: CreateQuery;
|
|
61
61
|
// Factory for creating an untracked query handle within the context of this provider.
|
|
62
62
|
createUntrackedQuery: CreateUntrackedQuery;
|
|
63
|
-
// Factory for creating an untracked mutation
|
|
63
|
+
// Factory for creating an untracked mutation handle within the context of this provider.
|
|
64
64
|
createUntrackedMutation: CreateUntrackedMutation;
|
|
65
65
|
// Convenience helper that shares dependency keys between tracked queries and mutations.
|
|
66
66
|
createQueryAndMutation: CreateQueryAndMutation;
|
|
@@ -126,7 +126,7 @@ export function setupQueryManager(queryClient: QueryClient): QueryManager {
|
|
|
126
126
|
> = <TData = unknown, TError = Error, TVariables = void, TOnMutateResult = unknown>(
|
|
127
127
|
mutationFn: MutationFunction<TData, TVariables>,
|
|
128
128
|
options?: Omit<
|
|
129
|
-
|
|
129
|
+
TrackedMutationHandleOptions<
|
|
130
130
|
Record<TDependencyKeys[number], TrackedDependencyValue>,
|
|
131
131
|
TData,
|
|
132
132
|
TError,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { act } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
4
|
+
|
|
5
|
+
import { setupMutation } from '../../mutation.js';
|
|
6
|
+
import { useMutationHandle } from '../hooks/use-mutation-handle.js';
|
|
7
|
+
|
|
8
|
+
import type { MutationHandle, MutationHandleSnapshot } from '../../mutation.js';
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('useMutationHandle', () => {
|
|
15
|
+
let container: HTMLDivElement;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
container = document.createElement('div');
|
|
23
|
+
document.body.appendChild(container);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
container.remove();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the latest mutation snapshot', async () => {
|
|
31
|
+
const queryClient = new QueryClient({ defaultOptions: { mutations: { retry: 0 } } });
|
|
32
|
+
const createMutation = setupMutation(queryClient);
|
|
33
|
+
const mutationFn = jest.fn((_payload: { name: string }) =>
|
|
34
|
+
Promise.resolve({ ok: true as const })
|
|
35
|
+
);
|
|
36
|
+
const handle = createMutation(mutationFn);
|
|
37
|
+
const statuses: string[] = [];
|
|
38
|
+
|
|
39
|
+
const Consumer = () => {
|
|
40
|
+
const snapshot = useMutationHandle(handle);
|
|
41
|
+
statuses.push(snapshot.status);
|
|
42
|
+
|
|
43
|
+
return <span>{snapshot.status}</span>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const root = createRoot(container);
|
|
47
|
+
|
|
48
|
+
await act(async () => {
|
|
49
|
+
root.render(<Consumer />);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(container.textContent).toBe('idle');
|
|
53
|
+
|
|
54
|
+
await act(async () => {
|
|
55
|
+
await handle.mutate({ name: 'Ada' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(container.textContent).toBe('success');
|
|
59
|
+
expect(statuses).toContain('idle');
|
|
60
|
+
expect(statuses).toContain('pending');
|
|
61
|
+
expect(statuses).toContain('success');
|
|
62
|
+
|
|
63
|
+
await act(async () => {
|
|
64
|
+
root.unmount();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('cleans up the store subscription on unmount', async () => {
|
|
69
|
+
const snapshot: MutationHandleSnapshot<{ ok: true }, Error, void> = {
|
|
70
|
+
data: { ok: true },
|
|
71
|
+
error: null,
|
|
72
|
+
status: 'success',
|
|
73
|
+
variables: undefined,
|
|
74
|
+
isError: false,
|
|
75
|
+
isIdle: false,
|
|
76
|
+
isPending: false,
|
|
77
|
+
isSuccess: true,
|
|
78
|
+
};
|
|
79
|
+
const unsubscribe = jest.fn();
|
|
80
|
+
const handle: MutationHandle<{ ok: true }, Error, void> = {
|
|
81
|
+
getSnapshot: () => snapshot,
|
|
82
|
+
subscribe: jest.fn(() => unsubscribe),
|
|
83
|
+
mutate: jest.fn(async () => ({ ok: true as const })),
|
|
84
|
+
reset: jest.fn(),
|
|
85
|
+
unsafe_getResult: jest.fn(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const root = createRoot(container);
|
|
89
|
+
|
|
90
|
+
const Consumer = () => {
|
|
91
|
+
useMutationHandle(handle);
|
|
92
|
+
return <span>ready</span>;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
root.render(<Consumer />);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(handle.subscribe).toHaveBeenCalledTimes(1);
|
|
100
|
+
|
|
101
|
+
await act(async () => {
|
|
102
|
+
root.unmount();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/react/hooks/index.ts
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Import the React hooks needed to memoize callbacks, hold mutable cache state,
|
|
2
|
+
// and connect an external store to React rendering.
|
|
3
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
// Import the mutation-handle contract and the stable snapshot shape the hook returns.
|
|
6
|
+
import type { MutationHandle, MutationHandleSnapshot } from '../../mutation.js';
|
|
7
|
+
// Describe the no-argument listener shape expected by useSyncExternalStore.
|
|
8
|
+
type Listener = () => void;
|
|
9
|
+
|
|
10
|
+
// Store one cached snapshot together with the store version it belongs to.
|
|
11
|
+
type SnapshotCacheEntry<TData, TError, TVariables> = {
|
|
12
|
+
// Hold the snapshot returned by the mutation handle for this version.
|
|
13
|
+
snapshot: MutationHandleSnapshot<TData, TError, TVariables>;
|
|
14
|
+
// Track which subscription version produced the cached snapshot.
|
|
15
|
+
version: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Subscribe a React component to a MutationHandle and return its latest snapshot.
|
|
19
|
+
export function useMutationHandle<TData, TError = Error, TVariables = void>(
|
|
20
|
+
// Receive the external mutation handle instance to subscribe to.
|
|
21
|
+
mutationHandle: MutationHandle<TData, TError, TVariables>
|
|
22
|
+
) {
|
|
23
|
+
// Count store notifications so we can tell when our cached snapshot is stale.
|
|
24
|
+
const snapshotVersionRef = useRef(0);
|
|
25
|
+
// Cache one client-side snapshot per observed version to keep getSnapshot stable.
|
|
26
|
+
const snapshotCacheRef = useRef<SnapshotCacheEntry<TData, TError, TVariables> | null>(null);
|
|
27
|
+
// Cache the server snapshot separately for the useSyncExternalStore SSR fallback.
|
|
28
|
+
const serverSnapshotCacheRef = useRef<MutationHandleSnapshot<TData, TError, TVariables> | null>(
|
|
29
|
+
null
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Create the subscribe function expected by useSyncExternalStore.
|
|
33
|
+
const subscribe = useCallback(
|
|
34
|
+
// React passes a listener that must run whenever the external store changes.
|
|
35
|
+
(listener: Listener) =>
|
|
36
|
+
// Forward the subscription to the mutation handle.
|
|
37
|
+
mutationHandle.subscribe(() => {
|
|
38
|
+
// Bump the version so later reads know the previous cache is outdated.
|
|
39
|
+
snapshotVersionRef.current += 1;
|
|
40
|
+
// Drop the cached client snapshot because the store just changed.
|
|
41
|
+
snapshotCacheRef.current = null;
|
|
42
|
+
// Drop the cached server snapshot for the same reason.
|
|
43
|
+
serverSnapshotCacheRef.current = null;
|
|
44
|
+
// Notify React that it should read a fresh snapshot.
|
|
45
|
+
listener();
|
|
46
|
+
}),
|
|
47
|
+
// Recreate the subscription function only when the handle instance changes.
|
|
48
|
+
[mutationHandle]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Read the current client snapshot in a referentially stable way for React.
|
|
52
|
+
const getSnapshot = useCallback(() => {
|
|
53
|
+
// Read the latest store version number.
|
|
54
|
+
const version = snapshotVersionRef.current;
|
|
55
|
+
// Read the last cached client snapshot, if there is one.
|
|
56
|
+
const cachedSnapshot = snapshotCacheRef.current;
|
|
57
|
+
|
|
58
|
+
// Reuse the cached snapshot when it was produced for the current version.
|
|
59
|
+
if (cachedSnapshot && cachedSnapshot.version === version) {
|
|
60
|
+
// Return the cached snapshot so repeated reads in the same render stay stable.
|
|
61
|
+
return cachedSnapshot.snapshot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ask the mutation handle for the latest snapshot because the cache is empty or stale.
|
|
65
|
+
const snapshot = mutationHandle.getSnapshot();
|
|
66
|
+
// Store the new snapshot together with the version it belongs to.
|
|
67
|
+
snapshotCacheRef.current = {
|
|
68
|
+
snapshot,
|
|
69
|
+
version,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Return the freshly read snapshot to React.
|
|
73
|
+
return snapshot;
|
|
74
|
+
}, [mutationHandle]);
|
|
75
|
+
|
|
76
|
+
// Read the server snapshot used by React during SSR or hydration fallback paths.
|
|
77
|
+
const getServerSnapshot = useCallback(() => {
|
|
78
|
+
// Read the cached server snapshot, if one was stored earlier.
|
|
79
|
+
const cachedSnapshot = serverSnapshotCacheRef.current;
|
|
80
|
+
|
|
81
|
+
// Reuse the cached server snapshot to keep server reads stable.
|
|
82
|
+
if (cachedSnapshot) {
|
|
83
|
+
// Return the cached server snapshot directly.
|
|
84
|
+
return cachedSnapshot;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ask the mutation handle for a snapshot because no server cache exists yet.
|
|
88
|
+
const snapshot = mutationHandle.getSnapshot();
|
|
89
|
+
// Cache that snapshot for the next server read.
|
|
90
|
+
serverSnapshotCacheRef.current = snapshot;
|
|
91
|
+
|
|
92
|
+
// Return the freshly read server snapshot.
|
|
93
|
+
return snapshot;
|
|
94
|
+
}, [mutationHandle]);
|
|
95
|
+
|
|
96
|
+
// Let React subscribe to the external store and read snapshots through the callbacks above.
|
|
97
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
98
|
+
}
|
package/src/react/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { useQueryHandle } from './hooks/index.js';
|
|
1
|
+
export { useQueryHandle, useMutationHandle } from './hooks/index.js';
|