From afd232671de77cf2bedc76edb96094c0949d425b Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 3 Sep 2021 13:23:56 +0200 Subject: [PATCH 1/5] add `reset` method to useMutation hook --- docs/rtk-query/api/created-api/hooks.mdx | 21 +++++----- .../toolkit/src/query/react/buildHooks.ts | 39 ++++++++---------- .../src/query/tests/buildHooks.test.tsx | 41 ++++++++++++++++++- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/docs/rtk-query/api/created-api/hooks.mdx b/docs/rtk-query/api/created-api/hooks.mdx index 0e589fd097..afd6d5c8e8 100644 --- a/docs/rtk-query/api/created-api/hooks.mdx +++ b/docs/rtk-query/api/created-api/hooks.mdx @@ -316,9 +316,9 @@ type UseMutationStateOptions = { selectFromResult?: (result: UseMutationStateDefaultResult) => any } -type UseMutationTrigger = ( - arg: any -) => Promise<{ data: T } | { error: BaseQueryError | SerializedError }> & { +type UseMutationTrigger = (arg: any) => Promise< + { data: T } | { error: BaseQueryError | SerializedError } +> & { requestId: string // A string generated by RTK Query abort: () => void // A method to cancel the mutation promise unwrap: () => Promise // A method to unwrap the mutation call and provide the raw response/error @@ -360,7 +360,10 @@ selectFromResult: () => ({}) - **Returns**: A tuple containing: - `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 - - `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 + - `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. + Additionally, this object will contain + - a `reset` method to reset the hook back to it's original state and remove the current result from the cache + - an `originalArgs` property that contain the argument passed to the last call of the `trigger` function. #### Description @@ -459,9 +462,8 @@ type UseQuerySubscriptionResult = { ## `useLazyQuery` ```ts title="Accessing a useLazyQuery hook" no-transpile -const [trigger, result, lastPromiseInfo] = api.endpoints.getPosts.useLazyQuery( - options -) +const [trigger, result, lastPromiseInfo] = + api.endpoints.getPosts.useLazyQuery(options) // or const [trigger, result, lastPromiseInfo] = api.useLazyGetPostsQuery(options) ``` @@ -520,9 +522,8 @@ type UseLazyQueryLastPromiseInfo = { ## `useLazyQuerySubscription` ```ts title="Accessing a useLazyQuerySubscription hook" no-transpile -const [trigger, lastArg] = api.endpoints.getPosts.useLazyQuerySubscription( - options -) +const [trigger, lastArg] = + api.endpoints.getPosts.useLazyQuerySubscription(options) ``` #### Signature diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index d956bb1a91..287d75e2bb 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -377,6 +377,11 @@ export type UseMutationStateResult< R > = NoInfer & { originalArgs?: QueryArgFrom + /** + * Resets the hook state to it's initial `uninitialized` state. + * This will also remove the last result from the cache. + */ + reset: () => void } /** @@ -717,47 +722,37 @@ export function buildHooks({ Definitions > const dispatch = useDispatch>() - const [requestId, setRequestId] = useState() - - const promiseRef = useRef>() + const [promise, setPromise] = useState>() - useEffect(() => { - return () => { - promiseRef.current?.unsubscribe() - promiseRef.current = undefined - } - }, []) + useEffect(() => () => promise?.unsubscribe(), [promise]) const triggerMutation = useCallback( function (arg) { - let promise: MutationActionCreatorResult - batch(() => { - promiseRef?.current?.unsubscribe() - promise = dispatch(initiate(arg)) - promiseRef.current = promise - setRequestId(promise.requestId) - }) - return promise! + const promise = dispatch(initiate(arg)) + setPromise(promise) + return promise }, [dispatch, initiate] ) const mutationSelector = useMemo( () => - createSelector([select(requestId || skipToken)], (subState) => - selectFromResult(subState) + createSelector( + [select(promise?.requestId || skipToken)], + selectFromResult ), - [select, requestId, selectFromResult] + [select, promise, selectFromResult] ) const currentState = useSelector(mutationSelector, shallowEqual) - const originalArgs = promiseRef.current?.arg.originalArgs + const originalArgs = promise?.arg.originalArgs const finalState = useMemo( () => ({ ...currentState, originalArgs, + reset: promise?.unsubscribe, }), - [currentState, originalArgs] + [currentState, originalArgs, promise] ) return useMemo( diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 18ea3ee57c..43f04abd38 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -869,6 +869,45 @@ describe('hooks tests', () => { expect(screen.queryByText(/Successfully updated user/i)).toBeNull() screen.getByText('Request was aborted') }) + + test('`reset` sets state back to original state', async () => { + function User() { + const [updateUser, result] = api.endpoints.updateUser.useMutation() + return ( + <> + + {result.isUninitialized + ? 'isUninitialized' + : result.isSuccess + ? 'isSuccess' + : 'other'} + + + + + ) + } + render(, { wrapper: storeRef.wrapper }) + + await screen.findByText(/isUninitialized/i) + expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( + 0 + ) + + userEvent.click(screen.getByRole('button', { name: 'trigger' })) + + await screen.findByText(/isSuccess/i) + expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( + 1 + ) + + userEvent.click(screen.getByRole('button', { name: 'reset' })) + + await screen.findByText(/isUninitialized/i) + expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( + 0 + ) + }) }) describe('usePrefetch', () => { @@ -1843,8 +1882,8 @@ describe('hooks with createApi defaults set', () => { api.internalActions.middlewareRegistered.match, increment.matchPending, increment.matchFulfilled, - api.internalActions.unsubscribeMutationResult.match, increment.matchPending, + api.internalActions.unsubscribeMutationResult.match, increment.matchFulfilled ) }) From b06b05298d5f79d9a28dd64631a6961161d6d0fa Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 3 Sep 2021 17:02:19 +0200 Subject: [PATCH 2/5] fix test --- packages/toolkit/src/query/tests/unionTypes.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolkit/src/query/tests/unionTypes.test.ts b/packages/toolkit/src/query/tests/unionTypes.test.ts index 87616715e7..07dde7a4cd 100644 --- a/packages/toolkit/src/query/tests/unionTypes.test.ts +++ b/packages/toolkit/src/query/tests/unionTypes.test.ts @@ -483,6 +483,7 @@ describe.skip('TS only tests', () => { isLoading: true, isSuccess: false, isError: false, + reset: () => {}, })(result) }) From 6d0968267abb7a8f5f767c97fcf05a22e8513a6b Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 17 Sep 2021 21:39:35 +0200 Subject: [PATCH 3/5] `unsubscribe` -> `reset` for mutations --- packages/toolkit/src/query/core/buildInitiate.ts | 14 ++++++++++---- .../query/core/buildMiddleware/cacheLifecycle.ts | 4 ++-- packages/toolkit/src/query/core/buildSlice.ts | 4 +++- packages/toolkit/src/query/react/buildHooks.ts | 14 +++++++------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index 9351098474..5fdfa32294 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -177,6 +177,8 @@ export type MutationActionCreatorResult< * A method to manually unsubscribe from the mutation call, meaning it will be removed from cache after the usual caching grace period. The value returned by the hook will reset to `isUninitialized` afterwards. */ + reset(): void + /** @deprecated has been renamed to `reset` */ unsubscribe(): void } @@ -193,7 +195,7 @@ export function buildInitiate({ }) { const { unsubscribeQueryResult, - unsubscribeMutationResult, + removeMutationResult, updateSubscriptionOptions, } = api.internalActions return { buildInitiateQuery, buildInitiateMutation } @@ -299,14 +301,18 @@ Features like automatic cache collection, automatic refetching etc. will not be .unwrap() .then((data) => ({ data })) .catch((error) => ({ error })) + + const reset = () => { + if (track) dispatch(removeMutationResult({ requestId })) + } + return Object.assign(returnValuePromise, { arg: thunkResult.arg, requestId, abort, unwrap: thunkResult.unwrap, - unsubscribe() { - if (track) dispatch(unsubscribeMutationResult({ requestId })) - }, + unsubscribe: reset, + reset, }) } } diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 52728a2d9d..bee25df289 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -235,7 +235,7 @@ export const build: SubMiddlewareBuilder = ({ } } else if ( api.internalActions.removeQueryResult.match(action) || - api.internalActions.unsubscribeMutationResult.match(action) + api.internalActions.removeMutationResult.match(action) ) { const lifecycle = lifecycleMap[cacheKey] if (lifecycle) { @@ -257,7 +257,7 @@ export const build: SubMiddlewareBuilder = ({ if (isMutationThunk(action)) return action.meta.requestId if (api.internalActions.removeQueryResult.match(action)) return action.payload.queryCacheKey - if (api.internalActions.unsubscribeMutationResult.match(action)) + if (api.internalActions.removeMutationResult.match(action)) return action.payload.requestId return '' } diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index e0616521e2..2465a6fc74 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -172,7 +172,7 @@ export function buildSlice({ name: `${reducerPath}/mutations`, initialState: initialState as MutationState, reducers: { - unsubscribeMutationResult( + removeMutationResult( draft, action: PayloadAction ) { @@ -379,6 +379,8 @@ export function buildSlice({ ...querySlice.actions, ...subscriptionSlice.actions, ...mutationSlice.actions, + /** @deprecated has been renamed to `removeMutationResult` */ + unsubscribeMutationResult: mutationSlice.actions.removeMutationResult, resetApiState, } diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 287d75e2bb..48eff0e9c7 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -735,13 +735,11 @@ export function buildHooks({ [dispatch, initiate] ) + const { requestId } = promise || {} const mutationSelector = useMemo( () => - createSelector( - [select(promise?.requestId || skipToken)], - selectFromResult - ), - [select, promise, selectFromResult] + createSelector([select(requestId || skipToken)], selectFromResult), + [select, requestId, selectFromResult] ) const currentState = useSelector(mutationSelector, shallowEqual) @@ -750,9 +748,11 @@ export function buildHooks({ () => ({ ...currentState, originalArgs, - reset: promise?.unsubscribe, + reset: () => + requestId && + dispatch(api.internalActions.removeMutationResult({ requestId })), }), - [currentState, originalArgs, promise] + [currentState, originalArgs, requestId, dispatch] ) return useMemo( From f37fe33db56390e64b1ff5c8ed311b470438c430 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 1 Oct 2021 17:15:03 +0200 Subject: [PATCH 4/5] minor adjustments --- .../toolkit/src/query/react/buildHooks.ts | 39 ++++++++++++------- .../src/query/tests/buildHooks.test.tsx | 31 ++++++++------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 4234dc097b..5e5aa11c2f 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -398,10 +398,28 @@ export type UseMutation> = < R extends Record = MutationResultSelectorResult >( options?: UseMutationStateOptions -) => [ - (arg: QueryArgFrom) => MutationActionCreatorResult, - UseMutationStateResult -] +) => [MutationTrigger, UseMutationStateResult] + +export type MutationTrigger> = + { + /** + * Triggers the mutation and returns a Promise. + * @remarks + * If you need to access the error or success payload immediately after a mutation, you can chain .unwrap(). + * + * @example + * ```ts + * // codeblock-meta title="Using .unwrap with async await" + * try { + * const payload = await addPost({ id: 1, name: 'Example' }).unwrap(); + * console.log('fulfilled', payload) + * } catch (error) { + * console.error('rejected', error); + * } + * ``` + */ + (arg: QueryArgFrom): MutationActionCreatorResult + } const defaultQueryStateSelector: QueryStateSelector = (x) => x const defaultMutationStateSelector: MutationStateSelector = (x) => x @@ -725,7 +743,7 @@ export function buildHooks({ const dispatch = useDispatch>() const [promise, setPromise] = useState>() - useEffect(() => () => promise?.unsubscribe(), [promise]) + useEffect(() => () => promise?.reset(), [promise]) const triggerMutation = useCallback( function (arg) { @@ -745,15 +763,10 @@ export function buildHooks({ const currentState = useSelector(mutationSelector, shallowEqual) const originalArgs = promise?.arg.originalArgs + const reset = useCallback(() => setPromise(undefined), []) const finalState = useMemo( - () => ({ - ...currentState, - originalArgs, - reset: () => - requestId && - dispatch(api.internalActions.removeMutationResult({ requestId })), - }), - [currentState, originalArgs, requestId, dispatch] + () => ({ ...currentState, originalArgs, reset }), + [currentState, originalArgs, reset] ) return useMemo( diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 81165151fd..fe6e44f9b2 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -906,6 +906,20 @@ describe('hooks tests', () => { screen.getByText('Request was aborted') }) + test('useMutation return value contains originalArgs', async () => { + const { result } = renderHook(api.endpoints.updateUser.useMutation, { + wrapper: storeRef.wrapper, + }) + const arg = { name: 'Foo' } + + const firstRenderResult = result.current + expect(firstRenderResult[1].originalArgs).toBe(undefined) + act(() => void firstRenderResult[0](arg)) + const secondRenderResult = result.current + expect(firstRenderResult[1].originalArgs).toBe(undefined) + expect(secondRenderResult[1].originalArgs).toBe(arg) + }) + test('`reset` sets state back to original state', async () => { function User() { const [updateUser, result] = api.endpoints.updateUser.useMutation() @@ -918,6 +932,7 @@ describe('hooks tests', () => { ? 'isSuccess' : 'other'} + {result.originalArgs?.name} @@ -926,6 +941,7 @@ describe('hooks tests', () => { render(, { wrapper: storeRef.wrapper }) await screen.findByText(/isUninitialized/i) + expect(screen.queryByText('Yay')).toBeNull() expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( 0 ) @@ -933,6 +949,7 @@ describe('hooks tests', () => { userEvent.click(screen.getByRole('button', { name: 'trigger' })) await screen.findByText(/isSuccess/i) + expect(screen.queryByText('Yay')).not.toBeNull() expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( 1 ) @@ -940,6 +957,7 @@ describe('hooks tests', () => { userEvent.click(screen.getByRole('button', { name: 'reset' })) await screen.findByText(/isUninitialized/i) + expect(screen.queryByText('Yay')).toBeNull() expect(Object.keys(storeRef.store.getState().api.mutations).length).toBe( 0 ) @@ -2004,19 +2022,6 @@ describe('hooks with createApi defaults set', () => { expect(getRenderCount()).toBe(5) }) - test('useMutation return value contains originalArgs', async () => { - const { result } = renderHook(api.endpoints.increment.useMutation, { - wrapper: storeRef.wrapper, - }) - - const firstRenderResult = result.current - expect(firstRenderResult[1].originalArgs).toBe(undefined) - firstRenderResult[0](5) - const secondRenderResult = result.current - expect(firstRenderResult[1].originalArgs).toBe(undefined) - expect(secondRenderResult[1].originalArgs).toBe(5) - }) - it('useMutation with selectFromResult option has a type error if the result is not an object', async () => { function Counter() { const [increment] = api.endpoints.increment.useMutation({ From 95bb6863cca1aaaad32aebfa8e063b74dfef73df Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 1 Oct 2021 17:15:52 +0200 Subject: [PATCH 5/5] Update docs/rtk-query/api/created-api/hooks.mdx Co-authored-by: Shrugsy --- docs/rtk-query/api/created-api/hooks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rtk-query/api/created-api/hooks.mdx b/docs/rtk-query/api/created-api/hooks.mdx index afd6d5c8e8..3be18cf59c 100644 --- a/docs/rtk-query/api/created-api/hooks.mdx +++ b/docs/rtk-query/api/created-api/hooks.mdx @@ -363,7 +363,7 @@ selectFromResult: () => ({}) - `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. Additionally, this object will contain - a `reset` method to reset the hook back to it's original state and remove the current result from the cache - - an `originalArgs` property that contain the argument passed to the last call of the `trigger` function. + - an `originalArgs` property that contains the argument passed to the last call of the `trigger` function. #### Description