diff --git a/src/useMutate.test.tsx b/src/useMutate.test.tsx index ce399a1..4220c04 100644 --- a/src/useMutate.test.tsx +++ b/src/useMutate.test.tsx @@ -34,6 +34,24 @@ describe("useMutate", () => { }); }); + it("should cancel on unmount", async () => { + nock("https://my-awesome-api.fake") + .delete("/plop") + .reply(200, { oops: true }); + const wrapper: React.FC = ({ children }) => ( + {children} + ); + + const resolve = jest.fn(() => "/plop"); + const { result, unmount } = renderHook(() => useMutate("DELETE", resolve), { wrapper }); + + const resultPromise = result.current.mutate({}); + unmount(); + const res = await resultPromise; + + expect(res).toEqual(undefined); + }); + it("should call the correct url with a specific id", async () => { nock("https://my-awesome-api.fake") .delete("/plop") @@ -419,7 +437,7 @@ describe("useMutate", () => { }); describe("Mutate identity", () => { - it("should remain the same across calls", async () => { + it("should remain the same across calls with static props", async () => { nock("https://my-awesome-api.fake") .post("/plop/one") .reply(200, { id: 1 }) @@ -447,6 +465,34 @@ describe("useMutate", () => { expect(mutate0).toEqual(mutate1); expect(mutate0).toEqual(mutate2); }); + + it("should remain the same across calls with deeply equal props", async () => { + nock("https://my-awesome-api.fake") + .post("/plop/one") + .reply(200, { id: 1 }) + .persist(); + + const wrapper: React.FC = ({ children }) => ( + {children} + ); + const getPath = ({ id }: { id: string }) => `plop/${id}`; + const { result } = renderHook( + () => + useMutate<{ id: number }, unknown, {}, {}, { id: string }>("POST", getPath, { + pathParams: { id: "one" }, + }), + { + wrapper, + }, + ); + const mutate0 = result.current.mutate; + const mutate1 = result.current.mutate; + await result.current.mutate({}); + const mutate2 = result.current.mutate; + + expect(mutate0).toEqual(mutate1); + expect(mutate0).toEqual(mutate2); + }); }); describe("Path Params", () => { diff --git a/src/useMutate.tsx b/src/useMutate.tsx index 11acff2..6bd6371 100644 --- a/src/useMutate.tsx +++ b/src/useMutate.tsx @@ -1,11 +1,12 @@ import merge from "lodash/merge"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { Context } from "./Context"; import { MutateMethod, MutateState, MutateRequestOptions } from "./Mutate"; import { Omit, UseGetProps } from "./useGet"; import { constructUrl } from "./util/constructUrl"; import { processResponse } from "./util/processResponse"; import { useAbort } from "./useAbort"; +import { useDeepCompareCallback, useDeepCompareEffect } from "./util/useDeepCompareEffect"; export interface UseMutateProps extends Omit, "lazy" | "debounce" | "mock"> { @@ -94,13 +95,32 @@ export function useMutate< useEffect(() => () => abort(), [abort]); const { pathInlineBodyEncode, queryParamStringifyOptions, requestOptions, localErrorOnly, onMutate } = props; - const mutate = useCallback>( + + const effectDependencies = [ + path, + pathParams, + queryParams, + verb, + isDelete, + base, + context, + queryParamStringifyOptions, + requestOptions, + onMutate, + abort, + pathInlineBodyEncode, + localErrorOnly, + resolve, + ]; + const mutate = useDeepCompareCallback>( async (body: TRequestBody, mutateRequestOptions?: MutateRequestOptions) => { + const signal = getAbortSignal(); + setState(prevState => { - if (prevState.loading) { - abort(); + if (prevState.error || !prevState.loading) { + return { ...prevState, loading: true, error: null }; } - return { ...prevState, loading: true, error: null }; + return prevState; }); const pathStr = @@ -129,8 +149,6 @@ export function useMutate< options.body = (body as unknown) as string; } - const signal = getAbortSignal(); - const url = constructUrl( base, pathParts.join("/"), @@ -233,25 +251,13 @@ export function useMutate< return data; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - /* getAbortSignal - changes too much! */ - path, - pathParams, - queryParams, - verb, - isDelete, - base, - context, - queryParamStringifyOptions, - requestOptions, - onMutate, - abort, - pathInlineBodyEncode, - localErrorOnly, - resolve, - ], + effectDependencies, ); + useDeepCompareEffect(() => { + if (state.loading) { + abort(); + } + }, effectDependencies); return { ...state,