authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-30 16:11:20-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-30 17:18:24-08:00
log30b230b64fca127fd768443d19b768ab5a7f88a2
tree620be03251f3a9aa70fd0053b5a7b1f88caf4816
parentea5d25fad4f41fa5631a62cb4de13ff35d6d5277
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

chore: readme maxing


2 files changed, 42 insertions(+), 30 deletions(-)

jsr.json+1-1
......@@ -1,6 +1,6 @@
11{
22 "name": "@clo/react-mutation",
3 "version": "1.0.0-rc.1",
3 "version": "1.0.0",
44 "exports": {
55 ".": "./src/mod.ts",
66 "./tanstack-query.ts": "./src/tanstack-query.ts",
readme.md+41-29
......@@ -10,16 +10,16 @@ their mutation story falls apart, is confusing, and misses a few obvious
1010features. Additionally, coworkers using AI agents continue to propagate bad
1111patterns and verbose code that is hard to review.
1212
13The primary gains React Mutation provides are:
14
15- **Automatic result handling**. If a `useMutate` hook does not observe
13- **Automatic result handling**. If a `useMutate` call does not observe
1614 `isError`, unhandled errors will be propagated to a global handler, which can
17 display a UI toast. Otherwise, the component can display the error locally.
15 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).
1816- **Optimistic helpers with built-in rollbacks** make it super easy to alter the
1917 UI without worrying about bugged error states. The
2018 [built in helpers for React Query](#react-query-optimistic-helpers) shows
2119 this power in more detail.
22- Extra treats such as debouncing (toggle button spam) and no-op filters (auto-save text inputs).
20- **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).
21
22On our work repository, switching to React Mutation reduced the line count of our mutations in half (rough estimate).
2323
2424## Setup
2525
......@@ -39,9 +39,10 @@ export const mutations = new MutationClient({
3939 // All properties in `context` are available within every function.
4040 context: {
4141 client: queryClient,
42 get: boundQueryClientGet(client),
43
4244 // Can add any easy helpers for your codebase.
43 // (btw, the correctly typed version of `get` is exported as `boundQueryClientGet(client)`)
44 get: (k: QueryKey) => client.getQueryData(k),
45 navigateAway: (urlThatIsBeingDeleted: string, redirect: string) => ...,
4546 },
4647
4748 // Optimistic helpers are a second type of context, only available within
......@@ -84,26 +85,35 @@ const mutDeleteItem = mutations.define({
8485 },
8586
8687 // `optimistic` is provided a `helpers` object which implement automatic rollbacks.
87 optimistic({ client, get, helpers, args: [id], onSuccess }) {
88 // Remove the matching items, but restore and refetch them on failure.
88 optimistic({ client, get, helpers, args: [id], onSuccess, onRestore, onRefetch }) {
89 // Remove the items matching the filter, but restore and refetch them on failure.
8990 helpers.arrayRemove(queryItemList, (item) => item === id);
90 // Remove this query from the client, but restore as stale and refetch it on failure.
91 helpers.removeQuery(queryItem);
91 // Set a property on an object, also restores and refetches. The `obj`
92 // helpers implement a type-safe object path system for nested fields.
93 helpers.objSet(queryItem(id), ["deleted"], true);
9294
9395 onSuccess((result) => {
94 // in general case, you may want to apply a success update
96 // Remove this query from the client
97 helpers.removeQuery(queryItem(id));
9598 });
99
100 onRestore(() => {}); // to restore non React Query state
101 onRefetch(() => {}); // to refetch non React Query state
102
103 // For React query specifically, there is a helper for refetching.
104 // internally, this is called from every other helper, and de-duplicates
105 // repeated calls so it only refetches once. This can be helpful if the data
106 // is only updated in `onSuccess` or the query is related in some way but
107 // doesn't have an optimistic update.
108 helpers.refetchOnSettled(queryItemList);
96109 },
97110
98111 // These strings are shown in error/success messages, called *after* optimistic state is applied.
99112 // Example: `Could not {description}`
100 describe({ get, args: [id] }) {
101 const title = get(queryItem().queryKey)?.title ?? "Unknown Item";
102 return `Delete '${get(queryItem(id))?.title ?? 'unknown'}'`;
103 },
113 describe: ({ get, args: [id] }) =>
114 `Delete '${get(queryItem(id))?.title ?? "Unknown Item"}'`,
104115 // Example: `Successfully {description}`
105 describeResult: ({ get, args: [id] }) =>
106 `Deleted '${get(queryItem(id))?.title ?? 'Unknown Item'}'`,
116 describeResult: ({ get, args: [id] }) => "Deleted Item",
107117
108118 // Since the optimistic handler is perfect, there is no need to refetch any
109119 // 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
137147#### React Query Optimistic Helpers
138148
139149When using React Query, you can opt into some incredible helpers for making it
140very easy to write Optimistic Updates. Our setup at work is with this client
150very easy to write optimistic updates. Our setup at work starts with this client
141151configuration.
142152
143153```ts
......@@ -157,6 +167,7 @@ export const mutations = new MutationClient({
157167 get: boundQueryClientGet(client),
158168 },
159169 getOptimisticHelpers: queryClientOptimisticHelpers(client),
170 // `showAlert` is our global toast function
160171 reportError(message) {
161172 showAlert(message, "error");
162173 },
......@@ -181,8 +192,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
181192 - `arrayFilter` - preserve items by a `filter` function
182193 - `arrayUpdate` - update items by a `filter` + `update` function
183194 - `arrayInsertIndex` - insert an item at an index
184- Queries that are complex objects. Each function takes a type-safe json path to
185 evaluate, but this system currently has type bugs and is being improved.
195- Queries that are complex objects. Each function takes a type-safe object path to
196 evaluate.
186197 - `objSet` - set a property
187198 - `objSetMany` - set many properties at once
188199 - `objIncrement` - increment a number
......@@ -198,7 +209,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
198209## Debouncing
199210
200211By default, a mutation will block the UI (by setting isPending). If you add
201`debounceMs`, the mutation will no longer set isPending. Multiple mutations
212`debounceMs`, the mutation will no longer set isPending. Consecutive mutations
213will override the earlier calls by rolling back the optimistic state.
202214
203215```tsx
204216const mutUpdateField = mutations.define({
......@@ -208,15 +220,15 @@ const mutUpdateField = mutations.define({
208220 },
209221 // (...describe functions...)
210222
211 // debounce for 0.5 seconds, grouping items on their `id`
212 debounceMs: 500,
213 key: ({ args: [id] }) => id,
223 debounceMs: 500, // wait 0.5 seconds before mutating
224 key: ({ args: [id] }) => id, // place same `ids` into the same timer group
225 // debounceImmediate: true, // can also support leading edge, good for buttons
214226});
215227
216228// React example - Auto-saving text field
217229function Item({ id }: { id: string }) {
218230 const { data: item } = useSuspenseQuery(queryItem(id));
219 const { run, isSuccess } = useMutate();
231 const { run, isSuccess } = useMutate(mutUpdateField);
220232
221233 return (
222234 <input
......@@ -225,7 +237,7 @@ function Item({ id }: { id: string }) {
225237 mutUpdateField.run(id, e.target.value);
226238 }}
227239 />
228 {}
240 {isSuccess ? "Saved" : null}
229241 );
230242}
231243```
......@@ -284,13 +296,13 @@ If the error is observed by the component, then React Mutation will know not to
284296invoke the global error handler. Same for success.
285297
286298```ts
287const { errorMeseage, isSuccess, run: run1 } = useMutate(...); // local handling in the form
299const { errorMessage, isSuccess, run: run1 } = useMutate(...); // local handling in the form
288300const { run: run2 } = useMutate(...); // global handling with alerts
289301
290302return (
291303 <>
292304 <button onClick={() => run1(...)}>local</button>
293 {isSuccess ? "you win!" : errorMeseage}
305 {isSuccess ? "you win!" : errorMessage}
294306
295307 <button onClick={() => run2(...)}>global</button>
296308 <>