diff --git a/jsr.json b/jsr.json index df5f363d0ad989c18637e77086deec33b96d00d0..1b4e5420fc5a0cd60bbf78e870097d1eb6a22ca7 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@clo/react-mutation", - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "exports": { ".": "./src/mod.ts", "./tanstack-query.ts": "./src/tanstack-query.ts", diff --git a/readme.md b/readme.md index 1a754a47843f561490ed379fbd7002cdb71aa47d..acb7ca8fc939ea5c74aff3b3081fbfea18446941 100644 --- a/readme.md +++ b/readme.md @@ -120,7 +120,6 @@ export function Example({ id }: { id: string }) { } ``` - ## Optimistic Updates The `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 - `onRestore` - add a callback to revert your optimistic update - `onRefetch` - add a callback to fetch data after a success -### React Query Optimistic Helpers +#### React Query Optimistic Helpers When using React Query, you can opt into some incredible helpers for making it very easy to write Optimistic Updates. Our setup at work is with this client @@ -228,6 +227,30 @@ function Item({ id }: { id: string }) { } ``` +## Snapshotting to Skip No-Ops + +For operations that might be passed a parameter that doesn't actually change +anything, `snapshot` can be used to detect no-op mutations. + +```tsx +const mutUpdateField = mutations.define({ + async mutate(id: string, value: string) { /* mutation */ }, + + optimistic({ args: [id, value], helpers }) { + helpers.objSet(queryItem(id), ["value"], value); + }, + + // called once before `optimistic` and once after. if the values are equal, + // then the mutation is cancelled (won't call `onSuccess`, but will `onSettled`) + // (defaulting to a json-based deep equal check, customize in MutationClient) + snapshot({ args: [id], get }) { + return get(queryItem(id))?.value; + } + + // (...describe and optionally debounce stuff...) +}); +``` + ## Calling Mutations Three methods exist for calling mutations: @@ -319,7 +342,7 @@ It can now be used for easy mutations: ## Batched Mutations -This is an advanced feature. Complete Documentation is pending. +This is an advanced feature. Complete Documentation is pending. It is not recommended to use this. Each call to the mutation applies new optimistic state on top of the previous, and after a debounce / throttle, the new optimistic state is committed to the diff --git a/src/blocking.ts b/src/blocking.ts index 45c01e577d2e42881e49420decbf14b0f5f73c8f..0ca7e409325dcfe7cb8d408d1360e4cb68856f76 100644 --- a/src/blocking.ts +++ b/src/blocking.ts @@ -292,7 +292,7 @@ export class BlockingMutation< // Check if debouncing is enabled if (this.#options.debounceMs !== undefined) { - return this.#runDebouncedAndReturn(args, key, channel, userOnRestore); + return this.#runDebouncedAndReturn(args, key, channel, userOnRestore, true); } // Create shared optimistic helpers instance for the channel if it doesn't exist @@ -466,6 +466,7 @@ export class BlockingMutation< key: string, channel: Channel, userOnRestore?: () => void, + fromRunWithOptions = false, ): Promise { // If there's a pending debounced call, roll it back if (channel.pendingDebounced) { @@ -624,13 +625,14 @@ export class BlockingMutation< reject: wrapperReject, } = Promise.withResolvers(); - // Add global handler logic for no-listener case + // Resolve/reject pending promises and add global handler logic for execution-time checks wrapperPromise.then( (result) => { // Resolve all pending promises pending.forEach((p) => p.resolve(result)); - // If no listeners, call global success handler + // Check if there are any listeners at execution time + const hasListeners = channel.listeners.size > 0; if (!hasListeners) { const message = this.describeResult(args, result); if (message && this.#client.reportSuccess) { @@ -642,7 +644,8 @@ export class BlockingMutation< // Reject all pending promises pending.forEach((p) => p.reject(error)); - // If no listeners, call global error handler + // Check if there are any listeners at execution time + const hasListeners = channel.listeners.size > 0; if (!hasListeners) { const message = `Failed to ${this.describe(...args)}: ${ errMessage(error)