authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 18:16:06-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-29 22:05:20-08:00
log4591d794aff944fbb510d589770021c9ccb60806
tree189aadc63be13ce6975bbd45a18a6363728664a5
parentbcb14cf5bd2f877987a158c4e40556f070172435
signaturelock-open Commit is signed but in an unrecognized format.

chore: stuff


3 files changed, 34 insertions(+), 8 deletions(-)

jsr.json+1-1
......@@ -1,6 +1,6 @@
11{
22 "name": "@clo/react-mutation",
3 "version": "1.0.0-beta.9",
3 "version": "1.0.0-beta.10",
44 "exports": {
55 ".": "./src/mod.ts",
66 "./tanstack-query.ts": "./src/tanstack-query.ts",
readme.md+26-3
......@@ -120,7 +120,6 @@ export function Example({ id }: { id: string }) {
120120}
121121```
122122
123
124123## Optimistic Updates
125124
126125The `optimistic` function is given an object with the following APIs
......@@ -132,7 +131,7 @@ The `optimistic` function is given an object with the following APIs
132131- `onRestore` - add a callback to revert your optimistic update
133132- `onRefetch` - add a callback to fetch data after a success
134133
135### React Query Optimistic Helpers
134#### React Query Optimistic Helpers
136135
137136When using React Query, you can opt into some incredible helpers for making it
138137very easy to write Optimistic Updates. Our setup at work is with this client
......@@ -228,6 +227,30 @@ function Item({ id }: { id: string }) {
228227}
229228```
230229
230## Snapshotting to Skip No-Ops
231
232For operations that might be passed a parameter that doesn't actually change
233anything, `snapshot` can be used to detect no-op mutations.
234
235```tsx
236const mutUpdateField = mutations.define({
237 async mutate(id: string, value: string) { /* mutation */ },
238
239 optimistic({ args: [id, value], helpers }) {
240 helpers.objSet(queryItem(id), ["value"], value);
241 },
242
243 // called once before `optimistic` and once after. if the values are equal,
244 // then the mutation is cancelled (won't call `onSuccess`, but will `onSettled`)
245 // (defaulting to a json-based deep equal check, customize in MutationClient)
246 snapshot({ args: [id], get }) {
247 return get(queryItem(id))?.value;
248 }
249
250 // (...describe and optionally debounce stuff...)
251});
252```
253
231254## Calling Mutations
232255
233256Three methods exist for calling mutations:
......@@ -319,7 +342,7 @@ It can now be used for easy mutations:
319342
320343## Batched Mutations
321344
322This is an advanced feature. Complete Documentation is pending.
345This is an advanced feature. Complete Documentation is pending. It is not recommended to use this.
323346
324347Each call to the mutation applies new optimistic state on top of the previous,
325348and after a debounce / throttle, the new optimistic state is committed to the
src/blocking.ts+7-4
......@@ -292,7 +292,7 @@ export class BlockingMutation<
292292
293293 // Check if debouncing is enabled
294294 if (this.#options.debounceMs !== undefined) {
295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore);
295 return this.#runDebouncedAndReturn(args, key, channel, userOnRestore, true);
296296 }
297297
298298 // Create shared optimistic helpers instance for the channel if it doesn't exist
......@@ -466,6 +466,7 @@ export class BlockingMutation<
466466 key: string,
467467 channel: Channel<Args, Result, Config["optimisticHelpers"]>,
468468 userOnRestore?: () => void,
469 fromRunWithOptions = false,
469470 ): Promise<Result> {
470471 // If there's a pending debounced call, roll it back
471472 if (channel.pendingDebounced) {
......@@ -624,13 +625,14 @@ export class BlockingMutation<
624625 reject: wrapperReject,
625626 } = Promise.withResolvers<Result>();
626627
627 // Add global handler logic for no-listener case
628 // Resolve/reject pending promises and add global handler logic for execution-time checks
628629 wrapperPromise.then(
629630 (result) => {
630631 // Resolve all pending promises
631632 pending.forEach((p) => p.resolve(result));
632633
633 // If no listeners, call global success handler
634 // Check if there are any listeners at execution time
635 const hasListeners = channel.listeners.size > 0;
634636 if (!hasListeners) {
635637 const message = this.describeResult(args, result);
636638 if (message && this.#client.reportSuccess) {
......@@ -642,7 +644,8 @@ export class BlockingMutation<
642644 // Reject all pending promises
643645 pending.forEach((p) => p.reject(error));
644646
645 // If no listeners, call global error handler
647 // Check if there are any listeners at execution time
648 const hasListeners = channel.listeners.size > 0;
646649 if (!hasListeners) {
647650 const message = `Failed to ${this.describe(...args)}: ${
648651 errMessage(error)