Skip to content

Commit e46b1de

Browse files
KyleAMathewsclaude
andauthored
Update mutation.md & JSDocs documentation for API changes (#743)
* docs: update paced mutations API to match final implementation Fixed the paced mutations documentation to reflect the final API from PR #704: - Updated all examples to use the onMutate callback pattern - Changed mutate() to accept variables directly instead of callbacks - Added comprehensive explanation of unique queues per hook instance - Clarified that each usePacedMutations() and createPacedMutations() call creates its own independent queue - Provided examples showing how to share queues across components for use cases like email draft auto-save This addresses the common confusion about whether mutations from different places share the same debounce/queue - they only do if you explicitly share the same instance. * docs: fix JSDoc examples for paced mutations Updated JSDoc comments to match the final paced mutations API: - debounceStrategy: Changed useSerializedTransaction to usePacedMutations and added onMutate example - throttleStrategy: Changed useSerializedTransaction to usePacedMutations and added onMutate examples - createPacedMutations: Fixed mutationFn signature - it only receives { transaction }, not the variables The mutationFn only receives transaction params, while onMutate receives the variables passed to mutate(). --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 48b8e8f commit e46b1de

File tree

4 files changed

+141
-39
lines changed

4 files changed

+141
-39
lines changed

docs/guides/mutations.md

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -925,8 +925,14 @@ The debounce strategy waits for a period of inactivity before persisting. This i
925925
```tsx
926926
import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
927927

928-
function AutoSaveForm() {
929-
const mutate = usePacedMutations({
928+
function AutoSaveForm({ formId }: { formId: string }) {
929+
const mutate = usePacedMutations<{ field: string; value: string }>({
930+
onMutate: ({ field, value }) => {
931+
// Apply optimistic update immediately
932+
formCollection.update(formId, (draft) => {
933+
draft[field] = value
934+
})
935+
},
930936
mutationFn: async ({ transaction }) => {
931937
// Persist the final merged state to the backend
932938
await api.forms.save(transaction.mutations)
@@ -937,11 +943,7 @@ function AutoSaveForm() {
937943

938944
const handleChange = (field: string, value: string) => {
939945
// Multiple rapid changes merge into a single transaction
940-
mutate(() => {
941-
formCollection.update(formId, (draft) => {
942-
draft[field] = value
943-
})
944-
})
946+
mutate({ field, value })
945947
}
946948

947949
return (
@@ -966,7 +968,13 @@ The throttle strategy ensures a minimum spacing between executions. This is idea
966968
import { usePacedMutations, throttleStrategy } from "@tanstack/react-db"
967969

968970
function VolumeSlider() {
969-
const mutate = usePacedMutations({
971+
const mutate = usePacedMutations<number>({
972+
onMutate: (volume) => {
973+
// Apply optimistic update immediately
974+
settingsCollection.update('volume', (draft) => {
975+
draft.value = volume
976+
})
977+
},
970978
mutationFn: async ({ transaction }) => {
971979
await api.settings.updateVolume(transaction.mutations)
972980
},
@@ -979,11 +987,7 @@ function VolumeSlider() {
979987
})
980988

981989
const handleVolumeChange = (volume: number) => {
982-
mutate(() => {
983-
settingsCollection.update('volume', (draft) => {
984-
draft.value = volume
985-
})
986-
})
990+
mutate(volume)
987991
}
988992

989993
return (
@@ -1010,7 +1014,15 @@ The queue strategy creates a separate transaction for each mutation and processe
10101014
import { usePacedMutations, queueStrategy } from "@tanstack/react-db"
10111015

10121016
function FileUploader() {
1013-
const mutate = usePacedMutations({
1017+
const mutate = usePacedMutations<File>({
1018+
onMutate: (file) => {
1019+
// Apply optimistic update immediately
1020+
uploadCollection.insert({
1021+
id: crypto.randomUUID(),
1022+
file,
1023+
status: 'pending',
1024+
})
1025+
},
10141026
mutationFn: async ({ transaction }) => {
10151027
// Each file upload is its own transaction
10161028
const mutation = transaction.mutations[0]
@@ -1026,14 +1038,8 @@ function FileUploader() {
10261038

10271039
const handleFileSelect = (files: FileList) => {
10281040
// Each file creates its own transaction, queued for sequential processing
1029-
Array.from(files).forEach((file, idx) => {
1030-
mutate(() => {
1031-
uploadCollection.insert({
1032-
id: crypto.randomUUID(),
1033-
file,
1034-
status: 'pending',
1035-
})
1036-
})
1041+
Array.from(files).forEach((file) => {
1042+
mutate(file)
10371043
})
10381044
}
10391045

@@ -1078,21 +1084,23 @@ The `usePacedMutations` hook makes it easy to use paced mutations in React compo
10781084
```tsx
10791085
import { usePacedMutations, debounceStrategy } from "@tanstack/react-db"
10801086

1081-
function MyComponent() {
1082-
const mutate = usePacedMutations({
1087+
function MyComponent({ itemId }: { itemId: string }) {
1088+
const mutate = usePacedMutations<number>({
1089+
onMutate: (newValue) => {
1090+
// Apply optimistic update immediately
1091+
collection.update(itemId, (draft) => {
1092+
draft.value = newValue
1093+
})
1094+
},
10831095
mutationFn: async ({ transaction }) => {
10841096
await api.save(transaction.mutations)
10851097
},
10861098
strategy: debounceStrategy({ wait: 500 }),
10871099
})
10881100

10891101
// Each mutate call returns a Transaction you can await
1090-
const handleSave = async () => {
1091-
const tx = mutate(() => {
1092-
collection.update(id, (draft) => {
1093-
draft.value = newValue
1094-
})
1095-
})
1102+
const handleSave = async (newValue: number) => {
1103+
const tx = mutate(newValue)
10961104

10971105
// Optionally wait for persistence
10981106
try {
@@ -1103,7 +1111,7 @@ function MyComponent() {
11031111
}
11041112
}
11051113

1106-
return <button onClick={handleSave}>Save</button>
1114+
return <button onClick={() => handleSave(42)}>Save</button>
11071115
}
11081116
```
11091117

@@ -1112,19 +1120,104 @@ The hook automatically memoizes the strategy and mutation function to prevent un
11121120
```ts
11131121
import { createPacedMutations, queueStrategy } from "@tanstack/db"
11141122

1115-
const { mutate } = createPacedMutations({
1123+
const mutate = createPacedMutations<{ id: string; changes: Partial<Item> }>({
1124+
onMutate: ({ id, changes }) => {
1125+
// Apply optimistic update immediately
1126+
collection.update(id, (draft) => {
1127+
Object.assign(draft, changes)
1128+
})
1129+
},
11161130
mutationFn: async ({ transaction }) => {
11171131
await api.save(transaction.mutations)
11181132
},
11191133
strategy: queueStrategy({ wait: 200 }),
11201134
})
11211135

11221136
// Use anywhere in your application
1123-
mutate(() => {
1124-
collection.update(id, updater)
1137+
mutate({ id: '123', changes: { name: 'New Name' } })
1138+
```
1139+
1140+
### Understanding Queues and Hook Instances
1141+
1142+
**Each unique `usePacedMutations` hook call creates its own independent queue.** This is an important design decision that affects how you structure your mutations.
1143+
1144+
If you have multiple components calling `usePacedMutations` separately, each will have its own isolated queue:
1145+
1146+
```tsx
1147+
function EmailDraftEditor1({ draftId }: { draftId: string }) {
1148+
// This creates Queue A
1149+
const mutate = usePacedMutations({
1150+
onMutate: (text) => {
1151+
draftCollection.update(draftId, (draft) => {
1152+
draft.text = text
1153+
})
1154+
},
1155+
mutationFn: async ({ transaction }) => {
1156+
await api.saveDraft(transaction.mutations)
1157+
},
1158+
strategy: debounceStrategy({ wait: 500 }),
1159+
})
1160+
1161+
return <textarea onChange={(e) => mutate(e.target.value)} />
1162+
}
1163+
1164+
function EmailDraftEditor2({ draftId }: { draftId: string }) {
1165+
// This creates Queue B (separate from Queue A)
1166+
const mutate = usePacedMutations({
1167+
onMutate: (text) => {
1168+
draftCollection.update(draftId, (draft) => {
1169+
draft.text = text
1170+
})
1171+
},
1172+
mutationFn: async ({ transaction }) => {
1173+
await api.saveDraft(transaction.mutations)
1174+
},
1175+
strategy: debounceStrategy({ wait: 500 }),
1176+
})
1177+
1178+
return <textarea onChange={(e) => mutate(e.target.value)} />
1179+
}
1180+
```
1181+
1182+
In this example, mutations from `EmailDraftEditor1` and `EmailDraftEditor2` will be queued and processed **independently**. They won't share the same debounce timer or queue.
1183+
1184+
**To share the same queue across multiple components**, create a single `createPacedMutations` instance and use it everywhere:
1185+
1186+
```tsx
1187+
// Create a single shared instance
1188+
import { createPacedMutations, debounceStrategy } from "@tanstack/db"
1189+
1190+
export const mutateDraft = createPacedMutations<{ draftId: string; text: string }>({
1191+
onMutate: ({ draftId, text }) => {
1192+
draftCollection.update(draftId, (draft) => {
1193+
draft.text = text
1194+
})
1195+
},
1196+
mutationFn: async ({ transaction }) => {
1197+
await api.saveDraft(transaction.mutations)
1198+
},
1199+
strategy: debounceStrategy({ wait: 500 }),
11251200
})
1201+
1202+
// Now both components share the same queue
1203+
function EmailDraftEditor1({ draftId }: { draftId: string }) {
1204+
return <textarea onChange={(e) => mutateDraft({ draftId, text: e.target.value })} />
1205+
}
1206+
1207+
function EmailDraftEditor2({ draftId }: { draftId: string }) {
1208+
return <textarea onChange={(e) => mutateDraft({ draftId, text: e.target.value })} />
1209+
}
11261210
```
11271211

1212+
With this approach, all mutations from both components share the same debounce timer and queue, ensuring they're processed in the correct order with a single debounce implementation.
1213+
1214+
**Key takeaways:**
1215+
1216+
- Each `usePacedMutations()` call = unique queue
1217+
- Each `createPacedMutations()` call = unique queue
1218+
- To share a queue: create one instance and import it everywhere you need it
1219+
- Shared queues ensure mutations from different places are ordered correctly
1220+
11281221
## Mutation Merging
11291222

11301223
When multiple mutations operate on the same item within a transaction, TanStack DB intelligently merges them to:

packages/db/src/paced-mutations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface PacedMutationsConfig<
5353
* // Apply optimistic update immediately
5454
* collection.update(id, draft => { draft.text = text })
5555
* },
56-
* mutationFn: async (text, { transaction }) => {
56+
* mutationFn: async ({ transaction }) => {
5757
* await api.save(transaction.mutations)
5858
* },
5959
* strategy: debounceStrategy({ wait: 500 })
@@ -73,7 +73,7 @@ export interface PacedMutationsConfig<
7373
* onMutate: ({ text }) => {
7474
* collection.insert({ id: uuid(), text, completed: false })
7575
* },
76-
* mutationFn: async ({ text }, { transaction }) => {
76+
* mutationFn: async ({ transaction }) => {
7777
* await api.save(transaction.mutations)
7878
* },
7979
* strategy: queueStrategy({

packages/db/src/strategies/debounceStrategy.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import type { Transaction } from "../transactions"
1414
*
1515
* @example
1616
* ```ts
17-
* const mutate = useSerializedTransaction({
17+
* const mutate = usePacedMutations({
18+
* onMutate: (value) => {
19+
* collection.update(id, draft => { draft.value = value })
20+
* },
1821
* mutationFn: async ({ transaction }) => {
1922
* await api.save(transaction.mutations)
2023
* },

packages/db/src/strategies/throttleStrategy.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import type { Transaction } from "../transactions"
1616
* @example
1717
* ```ts
1818
* // Throttle slider updates to every 200ms
19-
* const mutate = useSerializedTransaction({
19+
* const mutate = usePacedMutations({
20+
* onMutate: (volume) => {
21+
* settingsCollection.update('volume', draft => { draft.value = volume })
22+
* },
2023
* mutationFn: async ({ transaction }) => {
2124
* await api.updateVolume(transaction.mutations)
2225
* },
@@ -27,7 +30,10 @@ import type { Transaction } from "../transactions"
2730
* @example
2831
* ```ts
2932
* // Throttle with leading and trailing execution
30-
* const mutate = useSerializedTransaction({
33+
* const mutate = usePacedMutations({
34+
* onMutate: (data) => {
35+
* collection.update(id, draft => { Object.assign(draft, data) })
36+
* },
3137
* mutationFn: async ({ transaction }) => {
3238
* await api.save(transaction.mutations)
3339
* },

0 commit comments

Comments
 (0)