@tanstack/react-db 0.1.37 → 0.1.38
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/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/usePacedMutations.cjs +36 -0
- package/dist/cjs/usePacedMutations.cjs.map +1 -0
- package/dist/cjs/usePacedMutations.d.cts +90 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/usePacedMutations.d.ts +90 -0
- package/dist/esm/usePacedMutations.js +36 -0
- package/dist/esm/usePacedMutations.js.map +1 -0
- package/package.json +3 -2
- package/src/index.ts +1 -0
- package/src/usePacedMutations.ts +138 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const useLiveQuery = require("./useLiveQuery.cjs");
|
|
4
|
+
const usePacedMutations = require("./usePacedMutations.cjs");
|
|
4
5
|
const useLiveInfiniteQuery = require("./useLiveInfiniteQuery.cjs");
|
|
5
6
|
const db = require("@tanstack/db");
|
|
6
7
|
exports.useLiveQuery = useLiveQuery.useLiveQuery;
|
|
8
|
+
exports.usePacedMutations = usePacedMutations.usePacedMutations;
|
|
7
9
|
exports.useLiveInfiniteQuery = useLiveInfiniteQuery.useLiveInfiniteQuery;
|
|
8
10
|
Object.defineProperty(exports, "createTransaction", {
|
|
9
11
|
enumerable: true,
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const react = require("react");
|
|
4
|
+
const db = require("@tanstack/db");
|
|
5
|
+
function usePacedMutations(config) {
|
|
6
|
+
const onMutateRef = react.useRef(config.onMutate);
|
|
7
|
+
onMutateRef.current = config.onMutate;
|
|
8
|
+
const mutationFnRef = react.useRef(config.mutationFn);
|
|
9
|
+
mutationFnRef.current = config.mutationFn;
|
|
10
|
+
const stableOnMutate = react.useCallback((variables) => {
|
|
11
|
+
return onMutateRef.current(variables);
|
|
12
|
+
}, []);
|
|
13
|
+
const stableMutationFn = react.useCallback((params) => {
|
|
14
|
+
return mutationFnRef.current(params);
|
|
15
|
+
}, []);
|
|
16
|
+
const mutate = react.useMemo(() => {
|
|
17
|
+
return db.createPacedMutations({
|
|
18
|
+
...config,
|
|
19
|
+
onMutate: stableOnMutate,
|
|
20
|
+
mutationFn: stableMutationFn
|
|
21
|
+
});
|
|
22
|
+
}, [
|
|
23
|
+
stableOnMutate,
|
|
24
|
+
stableMutationFn,
|
|
25
|
+
config.metadata,
|
|
26
|
+
// Serialize strategy to avoid recreating when object reference changes but values are same
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
type: config.strategy._type,
|
|
29
|
+
options: config.strategy.options
|
|
30
|
+
})
|
|
31
|
+
]);
|
|
32
|
+
const stableMutate = react.useCallback(mutate, [mutate]);
|
|
33
|
+
return stableMutate;
|
|
34
|
+
}
|
|
35
|
+
exports.usePacedMutations = usePacedMutations;
|
|
36
|
+
//# sourceMappingURL=usePacedMutations.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePacedMutations.cjs","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":["useRef","useCallback","useMemo","createPacedMutations"],"mappings":";;;;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAcA,MAAAA,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgBA,MAAAA,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiBC,kBAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmBA,kBAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAASC,MAAAA,QAAQ,MAAM;AAC3B,WAAOC,wBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAeF,MAAAA,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;;"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PacedMutationsConfig, Transaction } from '@tanstack/db';
|
|
2
|
+
/**
|
|
3
|
+
* React hook for managing paced mutations with timing strategies.
|
|
4
|
+
*
|
|
5
|
+
* Provides optimistic mutations with pluggable strategies like debouncing,
|
|
6
|
+
* queuing, or throttling. The optimistic updates are applied immediately via
|
|
7
|
+
* `onMutate`, and the actual persistence is controlled by the strategy.
|
|
8
|
+
*
|
|
9
|
+
* @param config - Configuration including onMutate, mutationFn and strategy
|
|
10
|
+
* @returns A mutate function that accepts variables and returns Transaction objects
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Debounced auto-save
|
|
15
|
+
* function AutoSaveForm({ formId }: { formId: string }) {
|
|
16
|
+
* const mutate = usePacedMutations<string>({
|
|
17
|
+
* onMutate: (value) => {
|
|
18
|
+
* // Apply optimistic update immediately
|
|
19
|
+
* formCollection.update(formId, draft => {
|
|
20
|
+
* draft.content = value
|
|
21
|
+
* })
|
|
22
|
+
* },
|
|
23
|
+
* mutationFn: async ({ transaction }) => {
|
|
24
|
+
* await api.save(transaction.mutations)
|
|
25
|
+
* },
|
|
26
|
+
* strategy: debounceStrategy({ wait: 500 })
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* const handleChange = async (value: string) => {
|
|
30
|
+
* const tx = mutate(value)
|
|
31
|
+
*
|
|
32
|
+
* // Optional: await persistence or handle errors
|
|
33
|
+
* try {
|
|
34
|
+
* await tx.isPersisted.promise
|
|
35
|
+
* console.log('Saved!')
|
|
36
|
+
* } catch (error) {
|
|
37
|
+
* console.error('Save failed:', error)
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* return <textarea onChange={e => handleChange(e.target.value)} />
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Throttled slider updates
|
|
48
|
+
* function VolumeSlider() {
|
|
49
|
+
* const mutate = usePacedMutations<number>({
|
|
50
|
+
* onMutate: (volume) => {
|
|
51
|
+
* settingsCollection.update('volume', draft => {
|
|
52
|
+
* draft.value = volume
|
|
53
|
+
* })
|
|
54
|
+
* },
|
|
55
|
+
* mutationFn: async ({ transaction }) => {
|
|
56
|
+
* await api.updateVolume(transaction.mutations)
|
|
57
|
+
* },
|
|
58
|
+
* strategy: throttleStrategy({ wait: 200 })
|
|
59
|
+
* })
|
|
60
|
+
*
|
|
61
|
+
* return <input type="range" onChange={e => mutate(+e.target.value)} />
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // Debounce with leading/trailing for color picker (persist first + final only)
|
|
68
|
+
* function ColorPicker() {
|
|
69
|
+
* const mutate = usePacedMutations<string>({
|
|
70
|
+
* onMutate: (color) => {
|
|
71
|
+
* themeCollection.update('primary', draft => {
|
|
72
|
+
* draft.color = color
|
|
73
|
+
* })
|
|
74
|
+
* },
|
|
75
|
+
* mutationFn: async ({ transaction }) => {
|
|
76
|
+
* await api.updateTheme(transaction.mutations)
|
|
77
|
+
* },
|
|
78
|
+
* strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* return (
|
|
82
|
+
* <input
|
|
83
|
+
* type="color"
|
|
84
|
+
* onChange={e => mutate(e.target.value)}
|
|
85
|
+
* />
|
|
86
|
+
* )
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function usePacedMutations<TVariables = unknown, T extends object = Record<string, unknown>>(config: PacedMutationsConfig<TVariables, T>): (variables: TVariables) => Transaction<T>;
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useLiveQuery } from "./useLiveQuery.js";
|
|
2
|
+
import { usePacedMutations } from "./usePacedMutations.js";
|
|
2
3
|
import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
|
|
3
4
|
export * from "@tanstack/db";
|
|
4
5
|
import { createTransaction } from "@tanstack/db";
|
|
5
6
|
export {
|
|
6
7
|
createTransaction,
|
|
7
8
|
useLiveInfiniteQuery,
|
|
8
|
-
useLiveQuery
|
|
9
|
+
useLiveQuery,
|
|
10
|
+
usePacedMutations
|
|
9
11
|
};
|
|
10
12
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PacedMutationsConfig, Transaction } from '@tanstack/db';
|
|
2
|
+
/**
|
|
3
|
+
* React hook for managing paced mutations with timing strategies.
|
|
4
|
+
*
|
|
5
|
+
* Provides optimistic mutations with pluggable strategies like debouncing,
|
|
6
|
+
* queuing, or throttling. The optimistic updates are applied immediately via
|
|
7
|
+
* `onMutate`, and the actual persistence is controlled by the strategy.
|
|
8
|
+
*
|
|
9
|
+
* @param config - Configuration including onMutate, mutationFn and strategy
|
|
10
|
+
* @returns A mutate function that accepts variables and returns Transaction objects
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // Debounced auto-save
|
|
15
|
+
* function AutoSaveForm({ formId }: { formId: string }) {
|
|
16
|
+
* const mutate = usePacedMutations<string>({
|
|
17
|
+
* onMutate: (value) => {
|
|
18
|
+
* // Apply optimistic update immediately
|
|
19
|
+
* formCollection.update(formId, draft => {
|
|
20
|
+
* draft.content = value
|
|
21
|
+
* })
|
|
22
|
+
* },
|
|
23
|
+
* mutationFn: async ({ transaction }) => {
|
|
24
|
+
* await api.save(transaction.mutations)
|
|
25
|
+
* },
|
|
26
|
+
* strategy: debounceStrategy({ wait: 500 })
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* const handleChange = async (value: string) => {
|
|
30
|
+
* const tx = mutate(value)
|
|
31
|
+
*
|
|
32
|
+
* // Optional: await persistence or handle errors
|
|
33
|
+
* try {
|
|
34
|
+
* await tx.isPersisted.promise
|
|
35
|
+
* console.log('Saved!')
|
|
36
|
+
* } catch (error) {
|
|
37
|
+
* console.error('Save failed:', error)
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* return <textarea onChange={e => handleChange(e.target.value)} />
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Throttled slider updates
|
|
48
|
+
* function VolumeSlider() {
|
|
49
|
+
* const mutate = usePacedMutations<number>({
|
|
50
|
+
* onMutate: (volume) => {
|
|
51
|
+
* settingsCollection.update('volume', draft => {
|
|
52
|
+
* draft.value = volume
|
|
53
|
+
* })
|
|
54
|
+
* },
|
|
55
|
+
* mutationFn: async ({ transaction }) => {
|
|
56
|
+
* await api.updateVolume(transaction.mutations)
|
|
57
|
+
* },
|
|
58
|
+
* strategy: throttleStrategy({ wait: 200 })
|
|
59
|
+
* })
|
|
60
|
+
*
|
|
61
|
+
* return <input type="range" onChange={e => mutate(+e.target.value)} />
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // Debounce with leading/trailing for color picker (persist first + final only)
|
|
68
|
+
* function ColorPicker() {
|
|
69
|
+
* const mutate = usePacedMutations<string>({
|
|
70
|
+
* onMutate: (color) => {
|
|
71
|
+
* themeCollection.update('primary', draft => {
|
|
72
|
+
* draft.color = color
|
|
73
|
+
* })
|
|
74
|
+
* },
|
|
75
|
+
* mutationFn: async ({ transaction }) => {
|
|
76
|
+
* await api.updateTheme(transaction.mutations)
|
|
77
|
+
* },
|
|
78
|
+
* strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* return (
|
|
82
|
+
* <input
|
|
83
|
+
* type="color"
|
|
84
|
+
* onChange={e => mutate(e.target.value)}
|
|
85
|
+
* />
|
|
86
|
+
* )
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function usePacedMutations<TVariables = unknown, T extends object = Record<string, unknown>>(config: PacedMutationsConfig<TVariables, T>): (variables: TVariables) => Transaction<T>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useRef, useCallback, useMemo } from "react";
|
|
2
|
+
import { createPacedMutations } from "@tanstack/db";
|
|
3
|
+
function usePacedMutations(config) {
|
|
4
|
+
const onMutateRef = useRef(config.onMutate);
|
|
5
|
+
onMutateRef.current = config.onMutate;
|
|
6
|
+
const mutationFnRef = useRef(config.mutationFn);
|
|
7
|
+
mutationFnRef.current = config.mutationFn;
|
|
8
|
+
const stableOnMutate = useCallback((variables) => {
|
|
9
|
+
return onMutateRef.current(variables);
|
|
10
|
+
}, []);
|
|
11
|
+
const stableMutationFn = useCallback((params) => {
|
|
12
|
+
return mutationFnRef.current(params);
|
|
13
|
+
}, []);
|
|
14
|
+
const mutate = useMemo(() => {
|
|
15
|
+
return createPacedMutations({
|
|
16
|
+
...config,
|
|
17
|
+
onMutate: stableOnMutate,
|
|
18
|
+
mutationFn: stableMutationFn
|
|
19
|
+
});
|
|
20
|
+
}, [
|
|
21
|
+
stableOnMutate,
|
|
22
|
+
stableMutationFn,
|
|
23
|
+
config.metadata,
|
|
24
|
+
// Serialize strategy to avoid recreating when object reference changes but values are same
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
type: config.strategy._type,
|
|
27
|
+
options: config.strategy.options
|
|
28
|
+
})
|
|
29
|
+
]);
|
|
30
|
+
const stableMutate = useCallback(mutate, [mutate]);
|
|
31
|
+
return stableMutate;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
usePacedMutations
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=usePacedMutations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePacedMutations.js","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":[],"mappings":";;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgB,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiB,YAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmB,YAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAAS,QAAQ,MAAM;AAC3B,WAAO,qBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAe,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-db",
|
|
3
3
|
"description": "React integration for @tanstack/db",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.38",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"use-sync-external-store": "^1.6.0",
|
|
20
|
-
"@tanstack/db": "0.4.
|
|
20
|
+
"@tanstack/db": "0.4.16"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@electric-sql/client": "1.1.0",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"types": "dist/esm/index.d.ts",
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "vite build",
|
|
59
|
+
"build:minified": "vite build --minify",
|
|
59
60
|
"dev": "vite build --watch",
|
|
60
61
|
"test": "npx vitest --run",
|
|
61
62
|
"lint": "eslint . --fix"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react"
|
|
2
|
+
import { createPacedMutations } from "@tanstack/db"
|
|
3
|
+
import type { PacedMutationsConfig, Transaction } from "@tanstack/db"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React hook for managing paced mutations with timing strategies.
|
|
7
|
+
*
|
|
8
|
+
* Provides optimistic mutations with pluggable strategies like debouncing,
|
|
9
|
+
* queuing, or throttling. The optimistic updates are applied immediately via
|
|
10
|
+
* `onMutate`, and the actual persistence is controlled by the strategy.
|
|
11
|
+
*
|
|
12
|
+
* @param config - Configuration including onMutate, mutationFn and strategy
|
|
13
|
+
* @returns A mutate function that accepts variables and returns Transaction objects
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // Debounced auto-save
|
|
18
|
+
* function AutoSaveForm({ formId }: { formId: string }) {
|
|
19
|
+
* const mutate = usePacedMutations<string>({
|
|
20
|
+
* onMutate: (value) => {
|
|
21
|
+
* // Apply optimistic update immediately
|
|
22
|
+
* formCollection.update(formId, draft => {
|
|
23
|
+
* draft.content = value
|
|
24
|
+
* })
|
|
25
|
+
* },
|
|
26
|
+
* mutationFn: async ({ transaction }) => {
|
|
27
|
+
* await api.save(transaction.mutations)
|
|
28
|
+
* },
|
|
29
|
+
* strategy: debounceStrategy({ wait: 500 })
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* const handleChange = async (value: string) => {
|
|
33
|
+
* const tx = mutate(value)
|
|
34
|
+
*
|
|
35
|
+
* // Optional: await persistence or handle errors
|
|
36
|
+
* try {
|
|
37
|
+
* await tx.isPersisted.promise
|
|
38
|
+
* console.log('Saved!')
|
|
39
|
+
* } catch (error) {
|
|
40
|
+
* console.error('Save failed:', error)
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* return <textarea onChange={e => handleChange(e.target.value)} />
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // Throttled slider updates
|
|
51
|
+
* function VolumeSlider() {
|
|
52
|
+
* const mutate = usePacedMutations<number>({
|
|
53
|
+
* onMutate: (volume) => {
|
|
54
|
+
* settingsCollection.update('volume', draft => {
|
|
55
|
+
* draft.value = volume
|
|
56
|
+
* })
|
|
57
|
+
* },
|
|
58
|
+
* mutationFn: async ({ transaction }) => {
|
|
59
|
+
* await api.updateVolume(transaction.mutations)
|
|
60
|
+
* },
|
|
61
|
+
* strategy: throttleStrategy({ wait: 200 })
|
|
62
|
+
* })
|
|
63
|
+
*
|
|
64
|
+
* return <input type="range" onChange={e => mutate(+e.target.value)} />
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* // Debounce with leading/trailing for color picker (persist first + final only)
|
|
71
|
+
* function ColorPicker() {
|
|
72
|
+
* const mutate = usePacedMutations<string>({
|
|
73
|
+
* onMutate: (color) => {
|
|
74
|
+
* themeCollection.update('primary', draft => {
|
|
75
|
+
* draft.color = color
|
|
76
|
+
* })
|
|
77
|
+
* },
|
|
78
|
+
* mutationFn: async ({ transaction }) => {
|
|
79
|
+
* await api.updateTheme(transaction.mutations)
|
|
80
|
+
* },
|
|
81
|
+
* strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
|
|
82
|
+
* })
|
|
83
|
+
*
|
|
84
|
+
* return (
|
|
85
|
+
* <input
|
|
86
|
+
* type="color"
|
|
87
|
+
* onChange={e => mutate(e.target.value)}
|
|
88
|
+
* />
|
|
89
|
+
* )
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function usePacedMutations<
|
|
94
|
+
TVariables = unknown,
|
|
95
|
+
T extends object = Record<string, unknown>,
|
|
96
|
+
>(
|
|
97
|
+
config: PacedMutationsConfig<TVariables, T>
|
|
98
|
+
): (variables: TVariables) => Transaction<T> {
|
|
99
|
+
// Keep refs to the latest callbacks so we can call them without recreating the instance
|
|
100
|
+
const onMutateRef = useRef(config.onMutate)
|
|
101
|
+
onMutateRef.current = config.onMutate
|
|
102
|
+
|
|
103
|
+
const mutationFnRef = useRef(config.mutationFn)
|
|
104
|
+
mutationFnRef.current = config.mutationFn
|
|
105
|
+
|
|
106
|
+
// Create stable wrappers that always call the latest version
|
|
107
|
+
const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {
|
|
108
|
+
return onMutateRef.current(variables)
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {
|
|
112
|
+
return mutationFnRef.current(params)
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
// Create paced mutations instance with proper dependency tracking
|
|
116
|
+
// Serialize strategy for stable comparison since strategy objects are recreated on each render
|
|
117
|
+
const mutate = useMemo(() => {
|
|
118
|
+
return createPacedMutations<TVariables, T>({
|
|
119
|
+
...config,
|
|
120
|
+
onMutate: stableOnMutate,
|
|
121
|
+
mutationFn: stableMutationFn,
|
|
122
|
+
})
|
|
123
|
+
}, [
|
|
124
|
+
stableOnMutate,
|
|
125
|
+
stableMutationFn,
|
|
126
|
+
config.metadata,
|
|
127
|
+
// Serialize strategy to avoid recreating when object reference changes but values are same
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
type: config.strategy._type,
|
|
130
|
+
options: config.strategy.options,
|
|
131
|
+
}),
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
// Return stable mutate callback
|
|
135
|
+
const stableMutate = useCallback(mutate, [mutate])
|
|
136
|
+
|
|
137
|
+
return stableMutate
|
|
138
|
+
}
|