Skip to content

Commit b5ed969

Browse files
phryneasShrugsy
andauthored
add reset method to useMutation hook (#1476)
Co-authored-by: Shrugsy <joshfraser91@gmail.com>
1 parent da55b23 commit b5ed969

File tree

7 files changed

+123
-61
lines changed

7 files changed

+123
-61
lines changed

docs/rtk-query/api/created-api/hooks.mdx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,9 @@ type UseMutationStateOptions = {
316316
selectFromResult?: (result: UseMutationStateDefaultResult) => any
317317
}
318318

319-
type UseMutationTrigger<T> = (
320-
arg: any
321-
) => Promise<{ data: T } | { error: BaseQueryError | SerializedError }> & {
319+
type UseMutationTrigger<T> = (arg: any) => Promise<
320+
{ data: T } | { error: BaseQueryError | SerializedError }
321+
> & {
322322
requestId: string // A string generated by RTK Query
323323
abort: () => void // A method to cancel the mutation promise
324324
unwrap: () => Promise<T> // A method to unwrap the mutation call and provide the raw response/error
@@ -360,7 +360,10 @@ selectFromResult: () => ({})
360360
361361
- **Returns**: A tuple containing:
362362
- `trigger`: A function that triggers an update to the data based on the provided argument. The trigger function returns a promise with the properties shown above that may be used to handle the behavior of the promise
363-
- `mutationState`: A query status object containing the current loading state and metadata about the request, or the values returned by the `selectFromResult` option where applicable
363+
- `mutationState`: A query status object containing the current loading state and metadata about the request, or the values returned by the `selectFromResult` option where applicable.
364+
Additionally, this object will contain
365+
- a `reset` method to reset the hook back to it's original state and remove the current result from the cache
366+
- an `originalArgs` property that contains the argument passed to the last call of the `trigger` function.
364367
365368
#### Description
366369
@@ -459,9 +462,8 @@ type UseQuerySubscriptionResult = {
459462
## `useLazyQuery`
460463
461464
```ts title="Accessing a useLazyQuery hook" no-transpile
462-
const [trigger, result, lastPromiseInfo] = api.endpoints.getPosts.useLazyQuery(
463-
options
464-
)
465+
const [trigger, result, lastPromiseInfo] =
466+
api.endpoints.getPosts.useLazyQuery(options)
465467
// or
466468
const [trigger, result, lastPromiseInfo] = api.useLazyGetPostsQuery(options)
467469
```
@@ -520,9 +522,8 @@ type UseLazyQueryLastPromiseInfo = {
520522
## `useLazyQuerySubscription`
521523
522524
```ts title="Accessing a useLazyQuerySubscription hook" no-transpile
523-
const [trigger, lastArg] = api.endpoints.getPosts.useLazyQuerySubscription(
524-
options
525-
)
525+
const [trigger, lastArg] =
526+
api.endpoints.getPosts.useLazyQuerySubscription(options)
526527
```
527528

528529
#### Signature

packages/toolkit/src/query/core/buildInitiate.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ export type MutationActionCreatorResult<
177177
* A method to manually unsubscribe from the mutation call, meaning it will be removed from cache after the usual caching grace period.
178178
The value returned by the hook will reset to `isUninitialized` afterwards.
179179
*/
180+
reset(): void
181+
/** @deprecated has been renamed to `reset` */
180182
unsubscribe(): void
181183
}
182184

@@ -193,7 +195,7 @@ export function buildInitiate({
193195
}) {
194196
const {
195197
unsubscribeQueryResult,
196-
unsubscribeMutationResult,
198+
removeMutationResult,
197199
updateSubscriptionOptions,
198200
} = api.internalActions
199201
return { buildInitiateQuery, buildInitiateMutation }
@@ -299,14 +301,18 @@ Features like automatic cache collection, automatic refetching etc. will not be
299301
.unwrap()
300302
.then((data) => ({ data }))
301303
.catch((error) => ({ error }))
304+
305+
const reset = () => {
306+
if (track) dispatch(removeMutationResult({ requestId }))
307+
}
308+
302309
return Object.assign(returnValuePromise, {
303310
arg: thunkResult.arg,
304311
requestId,
305312
abort,
306313
unwrap: thunkResult.unwrap,
307-
unsubscribe() {
308-
if (track) dispatch(unsubscribeMutationResult({ requestId }))
309-
},
314+
unsubscribe: reset,
315+
reset,
310316
})
311317
}
312318
}

packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export const build: SubMiddlewareBuilder = ({
235235
}
236236
} else if (
237237
api.internalActions.removeQueryResult.match(action) ||
238-
api.internalActions.unsubscribeMutationResult.match(action)
238+
api.internalActions.removeMutationResult.match(action)
239239
) {
240240
const lifecycle = lifecycleMap[cacheKey]
241241
if (lifecycle) {
@@ -257,7 +257,7 @@ export const build: SubMiddlewareBuilder = ({
257257
if (isMutationThunk(action)) return action.meta.requestId
258258
if (api.internalActions.removeQueryResult.match(action))
259259
return action.payload.queryCacheKey
260-
if (api.internalActions.unsubscribeMutationResult.match(action))
260+
if (api.internalActions.removeMutationResult.match(action))
261261
return action.payload.requestId
262262
return ''
263263
}

packages/toolkit/src/query/core/buildSlice.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function buildSlice({
172172
name: `${reducerPath}/mutations`,
173173
initialState: initialState as MutationState<any>,
174174
reducers: {
175-
unsubscribeMutationResult(
175+
removeMutationResult(
176176
draft,
177177
action: PayloadAction<MutationSubstateIdentifier>
178178
) {
@@ -379,6 +379,8 @@ export function buildSlice({
379379
...querySlice.actions,
380380
...subscriptionSlice.actions,
381381
...mutationSlice.actions,
382+
/** @deprecated has been renamed to `removeMutationResult` */
383+
unsubscribeMutationResult: mutationSlice.actions.removeMutationResult,
382384
resetApiState,
383385
}
384386

packages/toolkit/src/query/react/buildHooks.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ export type UseMutationStateResult<
408408
R
409409
> = NoInfer<R> & {
410410
originalArgs?: QueryArgFrom<D>
411+
/**
412+
* Resets the hook state to it's initial `uninitialized` state.
413+
* This will also remove the last result from the cache.
414+
*/
415+
reset: () => void
411416
}
412417

413418
/**
@@ -424,10 +429,28 @@ export type UseMutation<D extends MutationDefinition<any, any, any, any>> = <
424429
R extends Record<string, any> = MutationResultSelectorResult<D>
425430
>(
426431
options?: UseMutationStateOptions<D, R>
427-
) => [
428-
(arg: QueryArgFrom<D>) => MutationActionCreatorResult<D>,
429-
UseMutationStateResult<D, R>
430-
]
432+
) => [MutationTrigger<D>, UseMutationStateResult<D, R>]
433+
434+
export type MutationTrigger<D extends MutationDefinition<any, any, any, any>> =
435+
{
436+
/**
437+
* Triggers the mutation and returns a Promise.
438+
* @remarks
439+
* If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
440+
*
441+
* @example
442+
* ```ts
443+
* // codeblock-meta title="Using .unwrap with async await"
444+
* try {
445+
* const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
446+
* console.log('fulfilled', payload)
447+
* } catch (error) {
448+
* console.error('rejected', error);
449+
* }
450+
* ```
451+
*/
452+
(arg: QueryArgFrom<D>): MutationActionCreatorResult<D>
453+
}
431454

432455
const defaultQueryStateSelector: QueryStateSelector<any, any> = (x) => x
433456
const defaultMutationStateSelector: MutationStateSelector<any, any> = (x) => x
@@ -764,47 +787,32 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
764787
Definitions
765788
>
766789
const dispatch = useDispatch<ThunkDispatch<any, any, AnyAction>>()
767-
const [requestId, setRequestId] = useState<string>()
790+
const [promise, setPromise] = useState<MutationActionCreatorResult<any>>()
768791

769-
const promiseRef = useRef<MutationActionCreatorResult<any>>()
770-
771-
useEffect(() => {
772-
return () => {
773-
promiseRef.current?.unsubscribe()
774-
promiseRef.current = undefined
775-
}
776-
}, [])
792+
useEffect(() => () => promise?.reset(), [promise])
777793

778794
const triggerMutation = useCallback(
779795
function (arg) {
780-
let promise: MutationActionCreatorResult<any>
781-
batch(() => {
782-
promiseRef?.current?.unsubscribe()
783-
promise = dispatch(initiate(arg))
784-
promiseRef.current = promise
785-
setRequestId(promise.requestId)
786-
})
787-
return promise!
796+
const promise = dispatch(initiate(arg))
797+
setPromise(promise)
798+
return promise
788799
},
789800
[dispatch, initiate]
790801
)
791802

803+
const { requestId } = promise || {}
792804
const mutationSelector = useMemo(
793805
() =>
794-
createSelector([select(requestId || skipToken)], (subState) =>
795-
selectFromResult(subState)
796-
),
806+
createSelector([select(requestId || skipToken)], selectFromResult),
797807
[select, requestId, selectFromResult]
798808
)
799809

800810
const currentState = useSelector(mutationSelector, shallowEqual)
801-
const originalArgs = promiseRef.current?.arg.originalArgs
811+
const originalArgs = promise?.arg.originalArgs
812+
const reset = useCallback(() => setPromise(undefined), [])
802813
const finalState = useMemo(
803-
() => ({
804-
...currentState,
805-
originalArgs,
806-
}),
807-
[currentState, originalArgs]
814+
() => ({ ...currentState, originalArgs, reset }),
815+
[currentState, originalArgs, reset]
808816
)
809817

810818
return useMemo(

packages/toolkit/src/query/tests/buildHooks.test.tsx

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,63 @@ describe('hooks tests', () => {
906906
expect(screen.queryByText(/Successfully updated user/i)).toBeNull()
907907
screen.getByText('Request was aborted')
908908
})
909+
910+
test('useMutation return value contains originalArgs', async () => {
911+
const { result } = renderHook(api.endpoints.updateUser.useMutation, {
912+
wrapper: storeRef.wrapper,
913+
})
914+
const arg = { name: 'Foo' }
915+
916+
const firstRenderResult = result.current
917+
expect(firstRenderResult[1].originalArgs).toBe(undefined)
918+
act(() => void firstRenderResult[0](arg))
919+
const secondRenderResult = result.current
920+
expect(firstRenderResult[1].originalArgs).toBe(undefined)
921+
expect(secondRenderResult[1].originalArgs).toBe(arg)
922+
})
923+
924+
test('`reset` sets state back to original state', async () => {
925+
function User() {
926+
const [updateUser, result] = api.endpoints.updateUser.useMutation()
927+
return (
928+
<>
929+
<span>
930+
{result.isUninitialized
931+
? 'isUninitialized'
932+
: result.isSuccess
933+
? 'isSuccess'
934+
: 'other'}
935+
</span>
936+
<span>{result.originalArgs?.name}</span>
937+
<button onClick={() => updateUser({ name: 'Yay' })}>trigger</button>
938+
<button onClick={result.reset}>reset</button>
939+
</>
940+
)
941+
}
942+
render(<User />, { wrapper: storeRef.wrapper })
943+
944+
await screen.findByText(/isUninitialized/i)
945+
expect(screen.queryByText('Yay')).toBeNull()
946+
expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
947+
0
948+
)
949+
950+
userEvent.click(screen.getByRole('button', { name: 'trigger' }))
951+
952+
await screen.findByText(/isSuccess/i)
953+
expect(screen.queryByText('Yay')).not.toBeNull()
954+
expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
955+
1
956+
)
957+
958+
userEvent.click(screen.getByRole('button', { name: 'reset' }))
959+
960+
await screen.findByText(/isUninitialized/i)
961+
expect(screen.queryByText('Yay')).toBeNull()
962+
expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe(
963+
0
964+
)
965+
})
909966
})
910967

911968
describe('usePrefetch', () => {
@@ -1880,8 +1937,8 @@ describe('hooks with createApi defaults set', () => {
18801937
api.internalActions.middlewareRegistered.match,
18811938
increment.matchPending,
18821939
increment.matchFulfilled,
1883-
api.internalActions.unsubscribeMutationResult.match,
18841940
increment.matchPending,
1941+
api.internalActions.unsubscribeMutationResult.match,
18851942
increment.matchFulfilled
18861943
)
18871944
})
@@ -1966,19 +2023,6 @@ describe('hooks with createApi defaults set', () => {
19662023
expect(getRenderCount()).toBe(5)
19672024
})
19682025

1969-
test('useMutation return value contains originalArgs', async () => {
1970-
const { result } = renderHook(api.endpoints.increment.useMutation, {
1971-
wrapper: storeRef.wrapper,
1972-
})
1973-
1974-
const firstRenderResult = result.current
1975-
expect(firstRenderResult[1].originalArgs).toBe(undefined)
1976-
firstRenderResult[0](5)
1977-
const secondRenderResult = result.current
1978-
expect(firstRenderResult[1].originalArgs).toBe(undefined)
1979-
expect(secondRenderResult[1].originalArgs).toBe(5)
1980-
})
1981-
19822026
it('useMutation with selectFromResult option has a type error if the result is not an object', async () => {
19832027
function Counter() {
19842028
const [increment] = api.endpoints.increment.useMutation({

packages/toolkit/src/query/tests/unionTypes.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ describe.skip('TS only tests', () => {
497497
isLoading: true,
498498
isSuccess: false,
499499
isError: false,
500+
reset: () => {},
500501
})(result)
501502
})
502503

0 commit comments

Comments
 (0)