authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-11 15:27:52-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-11 16:27:26-07:00
log4b09ff4dba5fad5b7e2b44a0bdc2789918f74561
treea864dc511e5a7281e78b633b9c2247492adfd226
parentec65067d04a72a51b71b63d3b4569843b8fd0dad
signaturebadge-check Signed by SSH key SHA256:cOKiuRFOeSRxne6EWgHtdQQSlBxjOXm2hOCFnCdLQbQ

fix: allow spreading `TanstackQueryOptimisticHelpers`


2 files changed, 76 insertions(+), 59 deletions(-)

src/tanstack-query.ts+59-59
......@@ -47,10 +47,10 @@ class TanstackQueryOptimisticHelpers {
4747 * Set the entire query data.
4848 * If the query doesn't exist, the new query is created.
4949 */
50 set<Data>(
50 set = <Data>(
5151 queryKey: QueryKeyAndFn<Data>,
5252 value: Data | ((prev: Data | undefined) => Data | undefined),
53 ) {
53 ) => {
5454 const prev = this.#get(queryKey);
5555
5656 const newValue = typeof value === "function"
......@@ -68,16 +68,16 @@ class TanstackQueryOptimisticHelpers {
6868 this.#set(queryKey, prev);
6969 }
7070 });
71 }
71 };
7272
7373 /**
7474 * Update the entire query data.
7575 * If the query doesn't exist, it cancels
7676 */
77 updateExisting<Data>(
77 updateExisting = <Data>(
7878 queryKey: QueryKeyAndFn<Data>,
7979 value: Data | ((prev: Data) => Data),
80 ) {
80 ) => {
8181 const prev = this.#get(queryKey);
8282 if (!prev) return;
8383
......@@ -89,14 +89,14 @@ class TanstackQueryOptimisticHelpers {
8989 this.#onRestore(() => {
9090 this.#set(queryKey, prev);
9191 });
92 }
92 };
9393
9494 /**
9595 * Mark a query as changed and schedule a refetch.
9696 * Does not modify any data.
9797 * If the query doesn't exist, the updater is skipped.
9898 */
99 refetchOnSettled(queryKey: QueryKeyAndFn) {
99 refetchOnSettled = (queryKey: QueryKeyAndFn) => {
100100 if (!queryKey.queryFn) return;
101101 const state = this.#client.getQueryCache().find({
102102 queryKey: queryKey.queryKey,
......@@ -109,19 +109,19 @@ class TanstackQueryOptimisticHelpers {
109109 await this.#client.invalidateQueries(queryKey);
110110 });
111111 } catch { /* Ignore expiry */ }
112 }
112 };
113113
114114 /**
115115 * Set a property at an object path.
116116 * If the query or path doesn't exist, the updater is skipped.
117117 */
118 objSet<Data extends object, const Path extends AllObjectPaths<Data>>(
118 objSet = <Data extends object, const Path extends AllObjectPaths<Data>>(
119119 queryKey: QueryKeyAndFn<Data>,
120120 path: Path,
121121 value:
122122 | Exclude<GetObjectPath<Data, Path>, Function>
123123 | ((prev: GetObjectPath<Data, Path>) => GetObjectPath<Data, Path>),
124 ) {
124 ) => {
125125 const prev = this.#get(queryKey);
126126 if (!prev) return;
127127 const { value: original, exists } = getPath(prev, path);
......@@ -145,20 +145,20 @@ class TanstackQueryOptimisticHelpers {
145145 this.#onRestore(() => {
146146 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
147147 });
148 }
148 };
149149
150150 /**
151151 * Increment a numeric property at an object path.
152152 * If the query or path doesn't exist, the updater is skipped.
153153 */
154 objIncrement<
154 objIncrement = <
155155 Data extends object,
156156 const Path extends AllObjectPaths<Data>,
157157 >(
158158 queryKey: QueryKeyAndFn<Data>,
159159 path: Path,
160160 amount: number = 1,
161 ) {
161 ) => {
162162 const prev = this.#get(queryKey);
163163 if (!prev) return;
164164 const { value: original, exists } = getPath(prev, path);
......@@ -171,20 +171,20 @@ class TanstackQueryOptimisticHelpers {
171171 this.#onRestore(() => {
172172 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
173173 });
174 }
174 };
175175
176176 /**
177177 * Decrement a numeric property at an object path.
178178 * If the query or path doesn't exist, the updater is skipped.
179179 */
180 objDecrement<
180 objDecrement = <
181181 Data extends object,
182182 const Path extends AllObjectPaths<Data>,
183183 >(
184184 queryKey: QueryKeyAndFn<Data>,
185185 path: Path,
186186 amount: number = 1,
187 ) {
187 ) => {
188188 const prev = this.#get(queryKey);
189189 if (!prev) return;
190190 const { value: original, exists } = getPath(prev, path);
......@@ -197,16 +197,16 @@ class TanstackQueryOptimisticHelpers {
197197 this.#onRestore(() => {
198198 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
199199 });
200 }
200 };
201201
202202 /**
203203 * Toggle a boolean property at an object path.
204204 * If the query or path doesn't exist, the updater is skipped.
205205 */
206 objToggle<Data extends object, const Path extends AllObjectPaths<Data>>(
206 objToggle = <Data extends object, const Path extends AllObjectPaths<Data>>(
207207 queryKey: QueryKeyAndFn<Data>,
208208 path: Path,
209 ) {
209 ) => {
210210 const prev = this.#get(queryKey);
211211 if (!prev) return;
212212 const { value: original, exists } = getPath(prev, path);
......@@ -219,17 +219,17 @@ class TanstackQueryOptimisticHelpers {
219219 this.#onRestore(() => {
220220 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
221221 });
222 }
222 };
223223
224224 /**
225225 * Shallow merge multiple properties at an object path.
226226 * If the query or path doesn't exist, the updater is skipped.
227227 */
228 objSetMany<Data extends object, const Path extends AllObjectPaths<Data>>(
228 objSetMany = <Data extends object, const Path extends AllObjectPaths<Data>>(
229229 queryKey: QueryKeyAndFn<Data>,
230230 path: Path,
231231 updates: Partial<GetObjectPath<Data, Path>>,
232 ) {
232 ) => {
233233 const prev = this.#get(queryKey);
234234 if (!prev) return;
235235 const { value: original, exists } = getPath(prev, path);
......@@ -248,13 +248,13 @@ class TanstackQueryOptimisticHelpers {
248248 this.#onRestore(() => {
249249 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
250250 });
251 }
251 };
252252
253253 /**
254254 * Push item(s) to the end of an array at an object path.
255255 * If the query or path doesn't exist, the updater is skipped.
256256 */
257 objArrayPush<
257 objArrayPush = <
258258 Data extends object,
259259 const Path extends AllObjectPaths<Data>,
260260 >(
......@@ -262,7 +262,7 @@ class TanstackQueryOptimisticHelpers {
262262 path: Path,
263263 ...items: GetObjectPath<Data, Path> extends Array<infer T> ? T[]
264264 : never
265 ) {
265 ) => {
266266 const prev = this.#get(queryKey);
267267 if (!prev) return;
268268 const { value: original, exists } = getPath(prev, path);
......@@ -276,13 +276,13 @@ class TanstackQueryOptimisticHelpers {
276276 this.#onRestore(() => {
277277 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
278278 });
279 }
279 };
280280
281281 /**
282282 * Add item(s) to the beginning of an array at an object path.
283283 * If the query or path doesn't exist, the updater is skipped.
284284 */
285 objArrayUnshift<
285 objArrayUnshift = <
286286 Data extends object,
287287 const Path extends AllObjectPaths<Data>,
288288 >(
......@@ -290,7 +290,7 @@ class TanstackQueryOptimisticHelpers {
290290 path: Path,
291291 ...items: GetObjectPath<Data, Path> extends Array<infer T> ? T[]
292292 : never
293 ) {
293 ) => {
294294 const prev = this.#get(queryKey);
295295 if (!prev) return;
296296 const { value: original, exists } = getPath(prev, path);
......@@ -304,13 +304,13 @@ class TanstackQueryOptimisticHelpers {
304304 this.#onRestore(() => {
305305 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
306306 });
307 }
307 };
308308
309309 /**
310310 * Remove items from an array that match `filter`.
311311 * If the query or path doesn't exist, the updater is skipped.
312312 */
313 objArrayRemove<
313 objArrayRemove = <
314314 Data extends object,
315315 const Path extends AllObjectPaths<Data>,
316316 >(
......@@ -320,7 +320,7 @@ class TanstackQueryOptimisticHelpers {
320320 item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never,
321321 index: number,
322322 ) => boolean,
323 ) {
323 ) => {
324324 const prev = this.#get(queryKey);
325325 if (!prev) return;
326326 const { value: original, exists } = getPath(prev, path);
......@@ -335,13 +335,13 @@ class TanstackQueryOptimisticHelpers {
335335 // TODO: splice items back in case original changed
336336 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
337337 });
338 }
338 };
339339
340340 /**
341341 * Filter items to just include items that match `filter`. This is the inverse of `objArrayRemove`
342342 * If the query or path doesn't exist, the updater is skipped.
343343 */
344 objArrayFilter<
344 objArrayFilter = <
345345 Data extends object,
346346 const Path extends AllObjectPaths<Data>,
347347 >(
......@@ -351,7 +351,7 @@ class TanstackQueryOptimisticHelpers {
351351 item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never,
352352 index: number,
353353 ) => boolean,
354 ) {
354 ) => {
355355 const prev = this.#get(queryKey);
356356 if (!prev) return;
357357 const { value: original, exists } = getPath(prev, path);
......@@ -366,13 +366,13 @@ class TanstackQueryOptimisticHelpers {
366366 // TODO: splice items back in case original changed
367367 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
368368 });
369 }
369 };
370370
371371 /**
372372 * Update items in an array that match a predicate.
373373 * If the query or path doesn't exist, the updater is skipped.
374374 */
375 objArrayUpdate<
375 objArrayUpdate = <
376376 Data extends object,
377377 const Path extends AllObjectPaths<Data>,
378378 >(
......@@ -387,7 +387,7 @@ class TanstackQueryOptimisticHelpers {
387387 item: GetObjectPath<Data, Path> extends Array<infer T> ? T : never,
388388 ) => GetObjectPath<Data, Path> extends Array<infer T> ? T : never;
389389 },
390 ) {
390 ) => {
391391 const prev = this.#get(queryKey);
392392 if (!prev) return;
393393 const { value: original, exists } = getPath(prev, path);
......@@ -402,13 +402,13 @@ class TanstackQueryOptimisticHelpers {
402402 // TODO: splice items back in case original changed
403403 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
404404 });
405 }
405 };
406406
407407 /**
408408 * Insert item(s) at a specific index in an array.
409409 * If the query or path doesn't exist, the updater is skipped.
410410 */
411 objArrayInsertIndex<
411 objArrayInsertIndex = <
412412 Data extends object,
413413 const Path extends AllObjectPaths<Data>,
414414 >(
......@@ -417,7 +417,7 @@ class TanstackQueryOptimisticHelpers {
417417 index: number,
418418 ...items: GetObjectPath<Data, Path> extends Array<infer T> ? T[]
419419 : never
420 ) {
420 ) => {
421421 const prev = this.#get(queryKey);
422422 if (!prev) return;
423423 const { value: original, exists } = getPath(prev, path);
......@@ -436,13 +436,13 @@ class TanstackQueryOptimisticHelpers {
436436 // TODO: splice items back in case original changed
437437 this.#set(queryKey, (obj) => obj ? setPath(obj, path, original) : obj);
438438 });
439 }
439 };
440440
441441 /**
442442 * Push item(s) to the end of an array at an object path.
443443 * If the query or path doesn't exist, the updater is skipped.
444444 */
445 arrayPush<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) {
445 arrayPush = <Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) => {
446446 const prev = this.#get(queryKey);
447447 if (!prev || !Array.isArray(prev)) return;
448448
......@@ -454,13 +454,13 @@ class TanstackQueryOptimisticHelpers {
454454 (old) => old ? old.filter((x) => !items.includes(x)) : old,
455455 );
456456 });
457 }
457 };
458458
459459 /**
460460 * Add item(s) to the beginning of an array at an object path.
461461 * If the query or path doesn't exist, the updater is skipped.
462462 */
463 arrayUnshift<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) {
463 arrayUnshift = <Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) => {
464464 const prev = this.#get(queryKey);
465465 if (!prev || !Array.isArray(prev)) return;
466466
......@@ -472,19 +472,19 @@ class TanstackQueryOptimisticHelpers {
472472 (old) => old ? old.filter((x) => !items.includes(x)) : old,
473473 );
474474 });
475 }
475 };
476476
477477 /**
478478 * Remove items from an array that match a `filter`.
479479 * If the query or path doesn't exist, the updater is skipped.
480480 */
481 arrayRemove<Data>(
481 arrayRemove = <Data>(
482482 queryKey: QueryKeyAndFn<Data[] | null>,
483483 filter: (
484484 item: Data,
485485 index: number,
486486 ) => boolean,
487 ) {
487 ) => {
488488 const prev = this.#get(queryKey);
489489 if (!prev || !Array.isArray(prev)) return;
490490
......@@ -494,19 +494,19 @@ class TanstackQueryOptimisticHelpers {
494494 // TODO: splice items back in case original changed
495495 this.#set(queryKey, prev);
496496 });
497 }
497 };
498498
499499 /**
500500 * Filter items to just include items that match `filter`. This is the inverse of `arrayRemove`
501501 * If the query or path doesn't exist, the updater is skipped.
502502 */
503 arrayFilter<Data>(
503 arrayFilter = <Data>(
504504 queryKey: QueryKeyAndFn<Data[] | null>,
505505 filter: (
506506 item: Data,
507507 index: number,
508508 ) => boolean,
509 ) {
509 ) => {
510510 const prev = this.#get(queryKey);
511511 if (!prev || !Array.isArray(prev)) return;
512512
......@@ -516,13 +516,13 @@ class TanstackQueryOptimisticHelpers {
516516 // TODO: splice items back in case original changed
517517 this.#set(queryKey, prev);
518518 });
519 }
519 };
520520
521521 /**
522522 * Update items in an array that match a `filter`.
523523 * If the query or path doesn't exist, the updater is skipped.
524524 */
525 arrayUpdate<Data>(
525 arrayUpdate = <Data>(
526526 queryKey: QueryKeyAndFn<Data[] | null>,
527527 {
528528 filter,
......@@ -531,7 +531,7 @@ class TanstackQueryOptimisticHelpers {
531531 filter?: (item: Data, index: number) => boolean;
532532 update: (item: Data) => Data;
533533 },
534 ) {
534 ) => {
535535 const prev = this.#get(queryKey);
536536 if (!prev || !Array.isArray(prev)) return;
537537
......@@ -541,17 +541,17 @@ class TanstackQueryOptimisticHelpers {
541541 // TODO: splice items back in case original changed
542542 this.#set(queryKey, prev);
543543 });
544 }
544 };
545545
546546 /**
547547 * Insert item(s) at a specific index in an array.
548548 * If the query or path doesn't exist, the updater is skipped.
549549 */
550 arrayInsertIndex<Data>(
550 arrayInsertIndex = <Data>(
551551 queryKey: QueryKeyAndFn<Data[] | null>,
552552 index: number,
553553 ...items: Data[]
554 ) {
554 ) => {
555555 const prev = this.#get(queryKey);
556556 if (!prev || !Array.isArray(prev)) return;
557557
......@@ -565,12 +565,12 @@ class TanstackQueryOptimisticHelpers {
565565 // TODO: splice items back in case original changed
566566 this.#set(queryKey, prev);
567567 });
568 }
568 };
569569
570570 /**
571571 * Remove a query from the cache entirely.
572572 */
573 removeQuery(queryKey: QueryKeyAndFn) {
573 removeQuery = (queryKey: QueryKeyAndFn) => {
574574 const prev = this.#get(queryKey);
575575 if (!prev) return;
576576
......@@ -578,7 +578,7 @@ class TanstackQueryOptimisticHelpers {
578578 this.#onRestore(() => {
579579 this.#set(queryKey, prev);
580580 });
581 }
581 };
582582}
583583
584584export function queryClientOptimisticHelpers(
test/tanstack-query-helpers.test.ts+17
......@@ -46,6 +46,23 @@ function createTestQueryClient() {
4646 return { client, queryTest };
4747}
4848
49test("helpers can be spread and retain bound this", () => {
50 const { client, queryTest } = createTestQueryClient();
51
52 const helpers = queryClientOptimisticHelpers(client)({
53 onRestore: () => {},
54 onRefetch: () => {},
55 });
56 const spreadHelpers = { ...helpers };
57
58 assertEquals(Object.keys(spreadHelpers).includes("objIncrement"), true);
59
60 spreadHelpers.objIncrement(queryTest, ["count"], 2);
61
62 const result = client.getQueryData<TestData>(queryTest.queryKey);
63 assertEquals(result?.count, 12);
64});
65
4966// ============================================================================
5067// set() tests
5168// ============================================================================