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 @@...@@ -1,6 +1,6 @@
1{1{
2 "name": "@clo/react-mutation",2 "name": "@clo/react-mutation",
3 "version": "1.0.0-rc.1",3 "version": "1.0.0",
4 "exports": {4 "exports": {
5 ".": "./src/mod.ts",5 ".": "./src/mod.ts",
6 "./tanstack-query.ts": "./src/tanstack-query.ts",6 "./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...@@ -10,16 +10,16 @@ their mutation story falls apart, is confusing, and misses a few obvious
10features. Additionally, coworkers using AI agents continue to propagate bad10features. Additionally, coworkers using AI agents continue to propagate bad
11patterns and verbose code that is hard to review.11patterns and verbose code that is hard to review.
1212
13The primary gains React Mutation provides are:13- **Automatic result handling**. If a `useMutate` call does not observe
14
15- **Automatic result handling**. If a `useMutate` hook does not observe
16 `isError`, unhandled errors will be propagated to a global handler, which can14 `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).
18- **Optimistic helpers with built-in rollbacks** make it super easy to alter the16- **Optimistic helpers with built-in rollbacks** make it super easy to alter the
19 UI without worrying about bugged error states. The17 UI without worrying about bugged error states. The
20 [built in helpers for React Query](#react-query-optimistic-helpers) shows18 [built in helpers for React Query](#react-query-optimistic-helpers) shows
21 this power in more detail.19 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
24## Setup24## Setup
2525
...@@ -39,9 +39,10 @@ export const mutations = new MutationClient({...@@ -39,9 +39,10 @@ export const mutations = new MutationClient({
39 // All properties in `context` are available within every function.39 // All properties in `context` are available within every function.
40 context: {40 context: {
41 client: queryClient,41 client: queryClient,
42 get: boundQueryClientGet(client),
43
42 // Can add any easy helpers for your codebase.44 // Can add any easy helpers for your codebase.
43 // (btw, the correctly typed version of `get` is exported as `boundQueryClientGet(client)`)45 navigateAway: (urlThatIsBeingDeleted: string, redirect: string) => ...,
44 get: (k: QueryKey) => client.getQueryData(k),
45 },46 },
4647
47 // Optimistic helpers are a second type of context, only available within48 // Optimistic helpers are a second type of context, only available within
...@@ -84,26 +85,35 @@ const mutDeleteItem = mutations.define({...@@ -84,26 +85,35 @@ const mutDeleteItem = mutations.define({
84 },85 },
85 86
86 // `optimistic` is provided a `helpers` object which implement automatic rollbacks.87 // `optimistic` is provided a `helpers` object which implement automatic rollbacks.
87 optimistic({ client, get, helpers, args: [id], onSuccess }) {88 optimistic({ client, get, helpers, args: [id], onSuccess, onRestore, onRefetch }) {
88 // Remove the matching items, but restore and refetch them on failure.89 // Remove the items matching the filter, but restore and refetch them on failure.
89 helpers.arrayRemove(queryItemList, (item) => item === id);90 helpers.arrayRemove(queryItemList, (item) => item === id);
90 // Remove this query from the client, but restore as stale and refetch it on failure.91 // Set a property on an object, also restores and refetches. The `obj`
91 helpers.removeQuery(queryItem);92 // helpers implement a type-safe object path system for nested fields.
93 helpers.objSet(queryItem(id), ["deleted"], true);
92 94
93 onSuccess((result) => {95 onSuccess((result) => {
94 // in general case, you may want to apply a success update96 // Remove this query from the client
97 helpers.removeQuery(queryItem(id));
95 });98 });
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);
96 },109 },
97 110
98 // These strings are shown in error/success messages, called *after* optimistic state is applied.111 // These strings are shown in error/success messages, called *after* optimistic state is applied.
99 // Example: `Could not {description}`112 // Example: `Could not {description}`
100 describe({ get, args: [id] }) {113 describe: ({ get, args: [id] }) =>
101 const title = get(queryItem().queryKey)?.title ?? "Unknown Item";114 `Delete '${get(queryItem(id))?.title ?? "Unknown Item"}'`,
102 return `Delete '${get(queryItem(id))?.title ?? 'unknown'}'`;
103 },
104 // Example: `Successfully {description}`115 // Example: `Successfully {description}`
105 describeResult: ({ get, args: [id] }) =>116 describeResult: ({ get, args: [id] }) => "Deleted Item",
106 `Deleted '${get(queryItem(id))?.title ?? 'Unknown Item'}'`,
107 117
108 // Since the optimistic handler is perfect, there is no need to refetch any118 // Since the optimistic handler is perfect, there is no need to refetch any
109 // data once a success case is hit. This defaults to false for simplicity.119 // 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...@@ -137,7 +147,7 @@ The `optimistic` function is given an object with the following APIs
137#### React Query Optimistic Helpers147#### React Query Optimistic Helpers
138148
139When using React Query, you can opt into some incredible helpers for making it149When 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 client150very easy to write optimistic updates. Our setup at work starts with this client
141configuration.151configuration.
142152
143```ts153```ts
...@@ -157,6 +167,7 @@ export const mutations = new MutationClient({...@@ -157,6 +167,7 @@ export const mutations = new MutationClient({
157 get: boundQueryClientGet(client),167 get: boundQueryClientGet(client),
158 },168 },
159 getOptimisticHelpers: queryClientOptimisticHelpers(client),169 getOptimisticHelpers: queryClientOptimisticHelpers(client),
170 // `showAlert` is our global toast function
160 reportError(message) {171 reportError(message) {
161 showAlert(message, "error");172 showAlert(message, "error");
162 },173 },
...@@ -181,8 +192,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list...@@ -181,8 +192,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
181 - `arrayFilter` - preserve items by a `filter` function192 - `arrayFilter` - preserve items by a `filter` function
182 - `arrayUpdate` - update items by a `filter` + `update` function193 - `arrayUpdate` - update items by a `filter` + `update` function
183 - `arrayInsertIndex` - insert an item at an index194 - `arrayInsertIndex` - insert an item at an index
184- Queries that are complex objects. Each function takes a type-safe json path to195- Queries that are complex objects. Each function takes a type-safe object path to
185 evaluate, but this system currently has type bugs and is being improved.196 evaluate.
186 - `objSet` - set a property197 - `objSet` - set a property
187 - `objSetMany` - set many properties at once198 - `objSetMany` - set many properties at once
188 - `objIncrement` - increment a number199 - `objIncrement` - increment a number
...@@ -198,7 +209,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list...@@ -198,7 +209,8 @@ automatically implement `onRefetch` and `onRestore` callbacks. The current list
198## Debouncing209## Debouncing
199210
200By default, a mutation will block the UI (by setting isPending). If you add211By default, a mutation will block the UI (by setting isPending). If you add
201`debounceMs`, the mutation will no longer set isPending. Multiple mutations212`debounceMs`, the mutation will no longer set isPending. Consecutive mutations
213will override the earlier calls by rolling back the optimistic state.
202214
203```tsx215```tsx
204const mutUpdateField = mutations.define({216const mutUpdateField = mutations.define({
...@@ -208,15 +220,15 @@ const mutUpdateField = mutations.define({...@@ -208,15 +220,15 @@ const mutUpdateField = mutations.define({
208 },220 },
209 // (...describe functions...)221 // (...describe functions...)
210222
211 // debounce for 0.5 seconds, grouping items on their `id`223 debounceMs: 500, // wait 0.5 seconds before mutating
212 debounceMs: 500,224 key: ({ args: [id] }) => id, // place same `ids` into the same timer group
213 key: ({ args: [id] }) => id,225 // debounceImmediate: true, // can also support leading edge, good for buttons
214});226});
215227
216// React example - Auto-saving text field228// React example - Auto-saving text field
217function Item({ id }: { id: string }) {229function Item({ id }: { id: string }) {
218 const { data: item } = useSuspenseQuery(queryItem(id));230 const { data: item } = useSuspenseQuery(queryItem(id));
219 const { run, isSuccess } = useMutate();231 const { run, isSuccess } = useMutate(mutUpdateField);
220232
221 return (233 return (
222 <input234 <input
...@@ -225,7 +237,7 @@ function Item({ id }: { id: string }) {...@@ -225,7 +237,7 @@ function Item({ id }: { id: string }) {
225 mutUpdateField.run(id, e.target.value);237 mutUpdateField.run(id, e.target.value);
226 }}238 }}
227 />239 />
228 {}240 {isSuccess ? "Saved" : null}
229 );241 );
230}242}
231```243```
...@@ -284,13 +296,13 @@ If the error is observed by the component, then React Mutation will know not to...@@ -284,13 +296,13 @@ If the error is observed by the component, then React Mutation will know not to
284invoke the global error handler. Same for success.296invoke the global error handler. Same for success.
285297
286```ts298```ts
287const { errorMeseage, isSuccess, run: run1 } = useMutate(...); // local handling in the form299const { errorMessage, isSuccess, run: run1 } = useMutate(...); // local handling in the form
288const { run: run2 } = useMutate(...); // global handling with alerts300const { run: run2 } = useMutate(...); // global handling with alerts
289301
290return (302return (
291 <>303 <>
292 <button onClick={() => run1(...)}>local</button>304 <button onClick={() => run1(...)}>local</button>
293 {isSuccess ? "you win!" : errorMeseage}305 {isSuccess ? "you win!" : errorMessage}
294 306
295 <button onClick={() => run2(...)}>global</button>307 <button onClick={() => run2(...)}>global</button>
296 <>308 <>