diff --git a/jsr.json b/jsr.json
index a6b09d6d4ef9b2fbb0af41ede470cdac995fc5c7..2b3b61224f8b597f7aedee0ec7156a02cb895de0 100644
--- a/jsr.json
+++ b/jsr.json
@@ -1,6 +1,6 @@
{
"name": "@clo/react-mutation",
- "version": "1.0.0-rc.1",
+ "version": "1.0.0",
"exports": {
".": "./src/mod.ts",
"./tanstack-query.ts": "./src/tanstack-query.ts",
diff --git a/readme.md b/readme.md
index f5a9618023d53cd3e5dd5214e04b38c86886a2fe..c52b2d7adc8dbb2f8b06030b1c9a7364c28affd9 100644
--- a/readme.md
+++ b/readme.md
@@ -10,16 +10,16 @@ their mutation story falls apart, is confusing, and misses a few obvious
features. Additionally, coworkers using AI agents continue to propagate bad
patterns and verbose code that is hard to review.
-The primary gains React Mutation provides are:
-
-- **Automatic result handling**. If a `useMutate` hook does not observe
+- **Automatic result handling**. If a `useMutate` call does not observe
`isError`, unhandled errors will be propagated to a global handler, which can
- display a UI toast. Otherwise, the component can display the error locally.
+ display a UI toast. Otherwise, the component can display the error locally. How this works is explained in [the `useMutate` docs section](#the-usemutate-hook).
- **Optimistic helpers with built-in rollbacks** make it super easy to alter the
UI without worrying about bugged error states. The
[built in helpers for React Query](#react-query-optimistic-helpers) shows
this power in more detail.
-- Extra treats such as debouncing (toggle button spam) and no-op filters (auto-save text inputs).
+- **Extra treats** such as [debouncing](#debounced-mutations) (to reduce repeated API calls changing a state back and forth) and [no-op snapshots](#snapshotting-to-skip-no-ops) (to detect when the state has not actually changed and no API call is necessary).
+
+On our work repository, switching to React Mutation reduced the line count of our mutations in half (rough estimate).
## Setup
@@ -39,9 +39,10 @@ export const mutations = new MutationClient({
// All properties in `context` are available within every function.
context: {
client: queryClient,
+ get: boundQueryClientGet(client),
+
// Can add any easy helpers for your codebase.
- // (btw, the correctly typed version of `get` is exported as `boundQueryClientGet(client)`)
- get: (k: QueryKey) => client.getQueryData(k),
+ navigateAway: (urlThatIsBeingDeleted: string, redirect: string) => ...,
},
// Optimistic helpers are a second type of context, only available within
@@ -84,26 +85,35 @@ const mutDeleteItem = mutations.define({
},
// `optimistic` is provided a `helpers` object which implement automatic rollbacks.
- optimistic({ client, get, helpers, args: [id], onSuccess }) {
- // Remove the matching items, but restore and refetch them on failure.
+ optimistic({ client, get, helpers, args: [id], onSuccess, onRestore, onRefetch }) {
+ // Remove the items matching the filter, but restore and refetch them on failure.
helpers.arrayRemove(queryItemList, (item) => item === id);
- // Remove this query from the client, but restore as stale and refetch it on failure.
- helpers.removeQuery(queryItem);
+ // Set a property on an object, also restores and refetches. The `obj`
+ // helpers implement a type-safe object path system for nested fields.
+ helpers.objSet(queryItem(id), ["deleted"], true);
onSuccess((result) => {
- // in general case, you may want to apply a success update
+ // Remove this query from the client
+ helpers.removeQuery(queryItem(id));
});
+
+ onRestore(() => {}); // to restore non React Query state
+ onRefetch(() => {}); // to refetch non React Query state
+
+ // For React query specifically, there is a helper for refetching.
+ // internally, this is called from every other helper, and de-duplicates
+ // repeated calls so it only refetches once. This can be helpful if the data
+ // is only updated in `onSuccess` or the query is related in some way but
+ // doesn't have an optimistic update.
+ helpers.refetchOnSettled(queryItemList);
},
// These strings are shown in error/success messages, called *after* optimistic state is applied.
// Example: `Could not {description}`
- describe({ get, args: [id] }) {
- const title = get(queryItem().queryKey)?.title ?? "Unknown Item";
- return `Delete '${get(queryItem(id))?.title ?? 'unknown'}'`;
- },
+ describe: ({ get, args: [id] }) =>
+ `Delete '${get(queryItem(id))?.title ?? "Unknown Item"}'`,
// Example: `Successfully {description}`
- describeResult: ({ get, args: [id] }) =>
- `Deleted '${get(queryItem(id))?.title ?? 'Unknown Item'}'`,
+ describeResult: ({ get, args: [id] }) => "Deleted Item",
// Since the optimistic handler is perfect, there is no need to refetch any
// data once a success case is hit. This defaults to false for simplicity.
@@ -137,7 +147,7 @@ The `optimistic` function is given an object with the following APIs
#### 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
+very easy to write optimistic updates. Our setup at work starts with this client
configuration.
```ts
@@ -157,6 +167,7 @@ export const mutations = new MutationClient({
get: boundQueryClientGet(client),
},
getOptimisticHelpers: queryClientOptimisticHelpers(client),
+ // `showAlert` is our global toast function
reportError(message) {
showAlert(message, "error");
},
@@ -181,8 +192,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
- `arrayFilter` - preserve items by a `filter` function
- `arrayUpdate` - update items by a `filter` + `update` function
- `arrayInsertIndex` - insert an item at an index
-- Queries that are complex objects. Each function takes a type-safe json path to
- evaluate, but this system currently has type bugs and is being improved.
+- Queries that are complex objects. Each function takes a type-safe object path to
+ evaluate.
- `objSet` - set a property
- `objSetMany` - set many properties at once
- `objIncrement` - increment a number
@@ -198,7 +209,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
## Debouncing
By default, a mutation will block the UI (by setting isPending). If you add
-`debounceMs`, the mutation will no longer set isPending. Multiple mutations
+`debounceMs`, the mutation will no longer set isPending. Consecutive mutations
+will override the earlier calls by rolling back the optimistic state.
```tsx
const mutUpdateField = mutations.define({
@@ -208,15 +220,15 @@ const mutUpdateField = mutations.define({
},
// (...describe functions...)
- // debounce for 0.5 seconds, grouping items on their `id`
- debounceMs: 500,
- key: ({ args: [id] }) => id,
+ debounceMs: 500, // wait 0.5 seconds before mutating
+ key: ({ args: [id] }) => id, // place same `ids` into the same timer group
+ // debounceImmediate: true, // can also support leading edge, good for buttons
});
// React example - Auto-saving text field
function Item({ id }: { id: string }) {
const { data: item } = useSuspenseQuery(queryItem(id));
- const { run, isSuccess } = useMutate();
+ const { run, isSuccess } = useMutate(mutUpdateField);
return (
- {}
+ {isSuccess ? "Saved" : null}
);
}
```
@@ -284,13 +296,13 @@ If the error is observed by the component, then React Mutation will know not to
invoke the global error handler. Same for success.
```ts
-const { errorMeseage, isSuccess, run: run1 } = useMutate(...); // local handling in the form
+const { errorMessage, isSuccess, run: run1 } = useMutate(...); // local handling in the form
const { run: run2 } = useMutate(...); // global handling with alerts
return (
<>
- {isSuccess ? "you win!" : errorMeseage}
+ {isSuccess ? "you win!" : errorMessage}
<>