diff --git a/src/hooks/use-async-effect.test.tsx b/src/hooks/use-async-effect.test.tsx new file mode 100644 index 0000000..b141344 --- /dev/null +++ b/src/hooks/use-async-effect.test.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { cleanup, render, waitFor } from "@testing-library/react"; +import { useAsyncEffect } from "./use-async-effect"; +import { CoreUtils } from "andculturecode-javascript-core"; +import { AsyncEffectCallback } from "../types/async-effect-callback-type"; + +describe("useAsyncEffect", () => { + const setupUseAsyncEffect = (asyncEffect: AsyncEffectCallback) => { + const TestComponent = () => { + useAsyncEffect(asyncEffect, []); + return null; + }; + + render(); + }; + + test("executes async method", async () => { + // Arrange + const mockedMethod = jest.fn(); + + // Act + setupUseAsyncEffect(async () => { + await CoreUtils.sleep(1); + mockedMethod(); + }); + + // Assert + await waitFor(() => expect(mockedMethod).toBeCalledTimes(1)); + await cleanup(); + }); + + test("executes cleanup method", async () => { + // Arrange + const mockedMethod = jest.fn(); + const mockedCleanupMethod = jest.fn(); + + // Act + setupUseAsyncEffect(async () => { + await CoreUtils.sleep(1); + mockedMethod(); + return mockedCleanupMethod; + }); + + // Assert + await waitFor(() => expect(mockedMethod).toBeCalledTimes(1)); + await cleanup(); + await waitFor(() => expect(mockedCleanupMethod).toBeCalledTimes(1)); + }); + + test("isMounted initially equals true", async () => { + // Arrange + let actualIsMountedValue: boolean = false; + const expectedIsMountedValue: boolean = true; + + // Act + setupUseAsyncEffect(async (isMounted) => { + actualIsMountedValue = isMounted(); + await CoreUtils.sleep(1); + }); + + // Assert + expect(actualIsMountedValue).toBe(expectedIsMountedValue); + await cleanup(); + }); + + test("isMounted equals false after cleanup", async () => { + // Arrange + let actualIsMountedValue: boolean; + const expectedIsMountedValue: boolean = false; + const mockedMethod = jest.fn(); + + // Act + setupUseAsyncEffect(async (isMounted) => { + await CoreUtils.sleep(1); + actualIsMountedValue = isMounted(); + mockedMethod(); + }); + + // Assert + await cleanup(); + await waitFor(() => expect(mockedMethod).toBeCalledTimes(1)); + expect(actualIsMountedValue).toBe(expectedIsMountedValue); + }); +}); diff --git a/src/hooks/use-async-effect.ts b/src/hooks/use-async-effect.ts new file mode 100644 index 0000000..6523565 --- /dev/null +++ b/src/hooks/use-async-effect.ts @@ -0,0 +1,37 @@ +import { useEffect, DependencyList, EffectCallback, useCallback } from "react"; +import { AsyncEffectCallback } from "../types/async-effect-callback-type"; + +/** + * Version of the useEffect hook that accepts an async function + * @export + * @param {AsyncEffectCallback} asyncEffect + * @param {DependencyList} deps + */ +export function useAsyncEffect( + asyncEffect: AsyncEffectCallback, + deps: DependencyList +) { + const asyncCallback = useCallback(asyncEffect, deps); + + useEffect(() => { + let cleanupMethod = () => {}; + let isMounted = true; + + async function runAsyncCallback() { + const result: ReturnType = await asyncCallback( + () => isMounted + ); + + if (typeof result === "function") { + cleanupMethod = result; + } + } + + runAsyncCallback(); + + return () => { + cleanupMethod(); + isMounted = false; + }; + }, [asyncCallback]); +} diff --git a/src/index.ts b/src/index.ts index 268c229..491b8d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export { Redirects, RedirectsProps } from "./components/routing/redirects"; // ----------------------------------------------------------------------------------------- export { makeCancellable } from "./hooks/make-cancellable"; +export { useAsyncEffect } from "./hooks/use-async-effect"; export { useCancellablePromise } from "./hooks/use-cancellable-promise"; export { useDebounce } from "./hooks/use-debounce"; export { useLocalization } from "./hooks/use-localization"; @@ -61,6 +62,7 @@ export { ServiceHookFactory } from "./services/service-hook-factory"; // #region Types // ----------------------------------------------------------------------------------------- +export { AsyncEffectCallback } from "./types/async-effect-callback-type"; export { BulkUpdateService } from "./types/bulk-update-service-type"; export { BulkUpdateServiceHook } from "./types/bulk-update-service-hook-type"; export { CreateService } from "./types/create-service-type"; diff --git a/src/types/async-effect-callback-type.ts b/src/types/async-effect-callback-type.ts new file mode 100644 index 0000000..314afe8 --- /dev/null +++ b/src/types/async-effect-callback-type.ts @@ -0,0 +1,8 @@ +import { EffectCallback } from "react"; + +/** + * Type defining the asyncEffect parameter from calling `useAsyncEffect()` + */ +export type AsyncEffectCallback = ( + isMounted: () => boolean +) => Promise>;