| author | |
| committer | |
| log | c384cb9c7ac09948889010176f298fb775481b4b |
| tree | 1569b005e34ffedbe1cb6c8cc5a70bdfd396cb5f |
| parent | 2486cbb9d095be3d97251f02f076d0e2a1cb0033 |
| signature |
4 files changed, 113 insertions(+), 18 deletions(-)
src/blocking.ts+10| ... | @@ -70,6 +70,8 @@ export type BlockingOptimisticContext< | ... | @@ -70,6 +70,8 @@ export type BlockingOptimisticContext< |
| 70 | onRestore: (cb: () => void) => void; | 70 | onRestore: (cb: () => void) => void; |
| 71 | /** Add an event listener to apply `Result` to the store. */ | 71 | /** Add an event listener to apply `Result` to the store. */ |
| 72 | onSuccess: (cb: (result: Result) => void) => void; | 72 | onSuccess: (cb: (result: Result) => void) => void; |
| 73 | /** Add an event listener to refetch data after mutation. */ | ||
| 74 | onRefetch: (cb: () => Promise<void>) => void; | ||
| 73 | }; | 75 | }; |
| 74 | 76 | ||
| 75 | interface BlockingChannel<Args extends unknown[], Result, OptimisticHelpers> { | 77 | interface BlockingChannel<Args extends unknown[], Result, OptimisticHelpers> { |
| ... | @@ -245,6 +247,14 @@ export class BlockingMutation< | ... | @@ -245,6 +247,14 @@ export class BlockingMutation< |
| 245 | } | 247 | } |
| 246 | onSuccess.push(cb); | 248 | onSuccess.push(cb); |
| 247 | }, | 249 | }, |
| 250 | onRefetch(cb) { | ||
| 251 | if (expired) { | ||
| 252 | throw new Error( | ||
| 253 | "Can only call onRefetch from within the optimistic update function.", | ||
| 254 | ); | ||
| 255 | } | ||
| 256 | channel.refetches.push(cb); | ||
| 257 | }, | ||
| 248 | }); | 258 | }); |
| 249 | } catch (error) { | 259 | } catch (error) { |
| 250 | expired = true; | 260 | expired = true; |
src/debounced.ts+7-2| ... | @@ -72,7 +72,9 @@ export interface DebouncedMutationOptions< | ... | @@ -72,7 +72,9 @@ export interface DebouncedMutationOptions< |
| 72 | /** | 72 | /** |
| 73 | * Refetch all of the data this mutation could have affected. | 73 | * Refetch all of the data this mutation could have affected. |
| 74 | */ | 74 | */ |
| 75 | refetch?: () => Promise<void>; | 75 | refetch?: ( |
| 76 | context: Config["context"] & { args: NoInfer<Args> }, | ||
| 77 | ) => Promise<void>; | ||
| 76 | } | 78 | } |
| 77 | 79 | ||
| 78 | export type DebouncedOptimisticContext<Config extends MutationClientConfig> = | 80 | export type DebouncedOptimisticContext<Config extends MutationClientConfig> = |
| ... | @@ -501,7 +503,10 @@ export class DebouncedMutation< | ... | @@ -501,7 +503,10 @@ export class DebouncedMutation< |
| 501 | this.#notify(channel, "refetching", result); | 503 | this.#notify(channel, "refetching", result); |
| 502 | // Call refetch and all refetch callbacks in parallel | 504 | // Call refetch and all refetch callbacks in parallel |
| 503 | Promise.allSettled([ | 505 | Promise.allSettled([ |
| 504 | this.#options.refetch?.(), | 506 | this.#options.refetch?.({ |
| 507 | ...this.#client.context, | ||
| 508 | args: firstArgs, | ||
| 509 | }), | ||
| 505 | ...refetchCallbacks.map((cb) => cb()), | 510 | ...refetchCallbacks.map((cb) => cb()), |
| 506 | ]).then((results) => { | 511 | ]).then((results) => { |
| 507 | // Report any errors from refetch or callbacks | 512 | // Report any errors from refetch or callbacks |
src/tanstack-query.ts+25-16| ... | @@ -49,17 +49,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -49,17 +49,7 @@ class TanstackQueryOptimisticHelpers { |
| 49 | if (result) { | 49 | if (result) { |
| 50 | this.#client.cancelQueries({ queryKey: query.queryKey, exact: true }) | 50 | this.#client.cancelQueries({ queryKey: query.queryKey, exact: true }) |
| 51 | .catch(() => {}); | 51 | .catch(() => {}); |
| 52 | const state = this.#client.getQueryCache().find({ | 52 | this.refetchOnSettled(query); |
| 53 | queryKey: query.queryKey, | ||
| 54 | exact: true, | ||
| 55 | }); | ||
| 56 | if (!state || this.#refetchHashes.includes(state.queryHash)) return; | ||
| 57 | this.#refetchHashes.push(state.queryHash); | ||
| 58 | try { | ||
| 59 | this.#onRefetch(async () => { | ||
| 60 | await this.#client.refetchQueries(query); | ||
| 61 | }); | ||
| 62 | } catch { /* Ignore expiry */ } | ||
| 63 | } | 53 | } |
| 64 | } | 54 | } |
| 65 | 55 | ||
| ... | @@ -111,6 +101,25 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -111,6 +101,25 @@ class TanstackQueryOptimisticHelpers { |
| 111 | }); | 101 | }); |
| 112 | } | 102 | } |
| 113 | 103 | ||
| 104 | /** | ||
| 105 | * Mark a query as changed and schedule a refetch. | ||
| 106 | * Does not modify any data. | ||
| 107 | * If the query doesn't exist, the updater is skipped. | ||
| 108 | */ | ||
| 109 | refetchOnSettled(queryKey: QueryKeyAndFn) { | ||
| 110 | const state = this.#client.getQueryCache().find({ | ||
| 111 | queryKey: queryKey.queryKey, | ||
| 112 | exact: true, | ||
| 113 | }); | ||
| 114 | if (!state || this.#refetchHashes.includes(state.queryHash)) return; | ||
| 115 | this.#refetchHashes.push(state.queryHash); | ||
| 116 | try { | ||
| 117 | this.#onRefetch(async () => { | ||
| 118 | await this.#client.refetchQueries(queryKey); | ||
| 119 | }); | ||
| 120 | } catch { /* Ignore expiry */ } | ||
| 121 | } | ||
| 122 | |||
| 114 | /** | 123 | /** |
| 115 | * Set a property at an object path. | 124 | * Set a property at an object path. |
| 116 | * If the query or path doesn't exist, the updater is skipped. | 125 | * If the query or path doesn't exist, the updater is skipped. |
| ... | @@ -413,7 +422,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -413,7 +422,7 @@ class TanstackQueryOptimisticHelpers { |
| 413 | * Push item(s) to the end of an array at an object path. | 422 | * Push item(s) to the end of an array at an object path. |
| 414 | * If the query or path doesn't exist, the updater is skipped. | 423 | * If the query or path doesn't exist, the updater is skipped. |
| 415 | */ | 424 | */ |
| 416 | arrayPush<Data>(queryKey: QueryKeyAndFn<Data[]>, ...items: Data[]) { | 425 | arrayPush<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) { |
| 417 | const prev = this.#get(queryKey); | 426 | const prev = this.#get(queryKey); |
| 418 | if (!prev || !Array.isArray(prev)) return; | 427 | if (!prev || !Array.isArray(prev)) return; |
| 419 | 428 | ||
| ... | @@ -431,7 +440,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -431,7 +440,7 @@ class TanstackQueryOptimisticHelpers { |
| 431 | * Add item(s) to the beginning of an array at an object path. | 440 | * Add item(s) to the beginning of an array at an object path. |
| 432 | * If the query or path doesn't exist, the updater is skipped. | 441 | * If the query or path doesn't exist, the updater is skipped. |
| 433 | */ | 442 | */ |
| 434 | arrayUnshift<Data>(queryKey: QueryKeyAndFn<Data[]>, ...items: Data[]) { | 443 | arrayUnshift<Data>(queryKey: QueryKeyAndFn<Data[] | null>, ...items: Data[]) { |
| 435 | const prev = this.#get(queryKey); | 444 | const prev = this.#get(queryKey); |
| 436 | if (!prev || !Array.isArray(prev)) return; | 445 | if (!prev || !Array.isArray(prev)) return; |
| 437 | 446 | ||
| ... | @@ -450,7 +459,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -450,7 +459,7 @@ class TanstackQueryOptimisticHelpers { |
| 450 | * If the query or path doesn't exist, the updater is skipped. | 459 | * If the query or path doesn't exist, the updater is skipped. |
| 451 | */ | 460 | */ |
| 452 | arrayRemove<Data>( | 461 | arrayRemove<Data>( |
| 453 | queryKey: QueryKeyAndFn<Data[]>, | 462 | queryKey: QueryKeyAndFn<Data[] | null>, |
| 454 | filter: ( | 463 | filter: ( |
| 455 | item: Data, | 464 | item: Data, |
| 456 | index: number, | 465 | index: number, |
| ... | @@ -472,7 +481,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -472,7 +481,7 @@ class TanstackQueryOptimisticHelpers { |
| 472 | * If the query or path doesn't exist, the updater is skipped. | 481 | * If the query or path doesn't exist, the updater is skipped. |
| 473 | */ | 482 | */ |
| 474 | arrayUpdate<Data>( | 483 | arrayUpdate<Data>( |
| 475 | queryKey: QueryKeyAndFn<Data[]>, | 484 | queryKey: QueryKeyAndFn<Data[] | null>, |
| 476 | { | 485 | { |
| 477 | filter, | 486 | filter, |
| 478 | update, | 487 | update, |
| ... | @@ -499,7 +508,7 @@ class TanstackQueryOptimisticHelpers { | ... | @@ -499,7 +508,7 @@ class TanstackQueryOptimisticHelpers { |
| 499 | * If the query or path doesn't exist, the updater is skipped. | 508 | * If the query or path doesn't exist, the updater is skipped. |
| 500 | */ | 509 | */ |
| 501 | arrayInsertIndex<Data>( | 510 | arrayInsertIndex<Data>( |
| 502 | queryKey: QueryKeyAndFn<Data[]>, | 511 | queryKey: QueryKeyAndFn<Data[] | null>, |
| 503 | index: number, | 512 | index: number, |
| 504 | ...items: Data[] | 513 | ...items: Data[] |
| 505 | ) { | 514 | ) { |
test/tanstack-query-helpers.test.ts+71| ... | @@ -779,6 +779,77 @@ test("removeQuery - should skip if query doesn't exist", () => { | ... | @@ -779,6 +779,77 @@ test("removeQuery - should skip if query doesn't exist", () => { |
| 779 | assertEquals(restoreFns.length, 0); | 779 | assertEquals(restoreFns.length, 0); |
| 780 | }); | 780 | }); |
| 781 | 781 | ||
| 782 | // ============================================================================ | ||
| 783 | // refetchOnSettled() tests | ||
| 784 | // ============================================================================ | ||
| 785 | |||
| 786 | test("refetchOnSettled - should schedule a refetch without modifying data", () => { | ||
| 787 | const { client, queryTest } = createTestQueryClient(); | ||
| 788 | const refetchFns: Array<() => void> = []; | ||
| 789 | const restoreFns: Array<() => void> = []; | ||
| 790 | |||
| 791 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 792 | onRestore: (fn) => restoreFns.push(fn), | ||
| 793 | onRefetch: (fn) => refetchFns.push(fn), | ||
| 794 | }); | ||
| 795 | |||
| 796 | const originalData = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 797 | |||
| 798 | helpers.refetchOnSettled(queryTest); | ||
| 799 | |||
| 800 | const result = client.getQueryData<TestData>(queryTest.queryKey); | ||
| 801 | // Data should not be modified | ||
| 802 | assertEquals(result, originalData); | ||
| 803 | |||
| 804 | // A refetch should have been scheduled | ||
| 805 | assertEquals(refetchFns.length, 1); | ||
| 806 | |||
| 807 | // No restore callbacks should be registered | ||
| 808 | assertEquals(restoreFns.length, 0); | ||
| 809 | }); | ||
| 810 | |||
| 811 | test("refetchOnSettled - should skip if query doesn't exist", () => { | ||
| 812 | const { client } = createTestQueryClient(); | ||
| 813 | const refetchFns: Array<() => void> = []; | ||
| 814 | const restoreFns: Array<() => void> = []; | ||
| 815 | |||
| 816 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 817 | onRestore: (fn) => restoreFns.push(fn), | ||
| 818 | onRefetch: (fn) => refetchFns.push(fn), | ||
| 819 | }); | ||
| 820 | |||
| 821 | const queryNonexistent = queryOptions({ | ||
| 822 | queryKey: ["nonexistent"], | ||
| 823 | queryFn: (): TestData => initialData, | ||
| 824 | }); | ||
| 825 | |||
| 826 | helpers.refetchOnSettled(queryNonexistent); | ||
| 827 | |||
| 828 | // No refetch should be scheduled for non-existent query | ||
| 829 | assertEquals(refetchFns.length, 0); | ||
| 830 | assertEquals(restoreFns.length, 0); | ||
| 831 | }); | ||
| 832 | |||
| 833 | test("refetchOnSettled - should deduplicate refetches for the same query", () => { | ||
| 834 | const { client, queryTest } = createTestQueryClient(); | ||
| 835 | const refetchFns: Array<() => void> = []; | ||
| 836 | const restoreFns: Array<() => void> = []; | ||
| 837 | |||
| 838 | const helpers = queryClientOptimisticHelpers(client)({ | ||
| 839 | onRestore: (fn) => restoreFns.push(fn), | ||
| 840 | onRefetch: (fn) => refetchFns.push(fn), | ||
| 841 | }); | ||
| 842 | |||
| 843 | // Call refetchOnSettled multiple times on the same query | ||
| 844 | helpers.refetchOnSettled(queryTest); | ||
| 845 | helpers.refetchOnSettled(queryTest); | ||
| 846 | helpers.refetchOnSettled(queryTest); | ||
| 847 | |||
| 848 | // Only one refetch should be scheduled (deduplicated by query hash) | ||
| 849 | assertEquals(refetchFns.length, 1); | ||
| 850 | assertEquals(restoreFns.length, 0); | ||
| 851 | }); | ||
| 852 | |||
| 782 | // ============================================================================ | 853 | // ============================================================================ |
| 783 | // Integration tests | 854 | // Integration tests |
| 784 | // ============================================================================ | 855 | // ============================================================================ |