diff --git a/gitbook/reactjrx/SUMMARY.md b/gitbook/reactjrx/SUMMARY.md index 8f76371..e6d2c1a 100644 --- a/gitbook/reactjrx/SUMMARY.md +++ b/gitbook/reactjrx/SUMMARY.md @@ -43,6 +43,7 @@ * [React](utils/react/README.md) * [useMount](utils/react/usemount.md) * [useEffectOnce](utils/react/useeffectonce.md) + * [useUnmount](utils/react/useunmount.md) * [useConstant](utils/react/useconstant.md) * [useLiveRef](utils/react/useliveref.md) * [useDebounced](utils/react/usedebounced.md) diff --git a/gitbook/reactjrx/utils/react/useeffectonce.md b/gitbook/reactjrx/utils/react/useeffectonce.md index ed59fd7..2a6955c 100644 --- a/gitbook/reactjrx/utils/react/useeffectonce.md +++ b/gitbook/reactjrx/utils/react/useeffectonce.md @@ -1,6 +1,10 @@ # useEffectOnce -React lifecycle hook that runs an effect only once. +React lifecycle hook that runs an effect once, when the component mounts. + +{% hint style="warning" %} +In development, under ``, React mounts each component twice. So the effect runs twice, with its cleanup between the two runs. Make the cleanup undo the effect. +{% endhint %} ### Usage diff --git a/gitbook/reactjrx/utils/react/usemount.md b/gitbook/reactjrx/utils/react/usemount.md index 684e30a..0946aa1 100644 --- a/gitbook/reactjrx/utils/react/usemount.md +++ b/gitbook/reactjrx/utils/react/usemount.md @@ -2,6 +2,10 @@ React lifecycle hook that calls a function after the component is mounted. +{% hint style="warning" %} +In development, under ``, React mounts each component twice. So `useMount` calls `fn` twice. Make `fn` safe to call twice, or use `useEffectOnce` and undo the work in its cleanup. +{% endhint %} + ### Usage ```jsx diff --git a/gitbook/reactjrx/utils/react/useunmount.md b/gitbook/reactjrx/utils/react/useunmount.md new file mode 100644 index 0000000..a24c88a --- /dev/null +++ b/gitbook/reactjrx/utils/react/useunmount.md @@ -0,0 +1,24 @@ +# useUnmount + +React lifecycle hook that calls a function when the component unmounts. It calls the `fn` of the last committed render. + +{% hint style="warning" %} +In development, under ``, React mounts each component twice. So `useUnmount` also calls `fn` once after the first mount, while the component stays mounted. Do not end work there that the component still uses, such as a subject that it reads. +{% endhint %} + +### Usage + +```jsx +import {useUnmount} from 'reactjrx'; + +const Demo = ({ onClose }) => { + useUnmount(() => onClose()); + return null; +}; +``` + +### Reference + +```ts +useUnmount(fn: () => any); +``` diff --git a/src/lib/utils/react/useUnmount.test.tsx b/src/lib/utils/react/useUnmount.test.tsx new file mode 100644 index 0000000..58ab3e0 --- /dev/null +++ b/src/lib/utils/react/useUnmount.test.tsx @@ -0,0 +1,60 @@ +import { render, renderHook } from "@testing-library/react" +import { act, Suspense, startTransition, use, useState } from "react" +import { describe, expect, it, vi } from "vitest" +import { useUnmount } from "./useUnmount" + +describe("useUnmount", () => { + it("should call the fn of the last render when the component unmounts", () => { + const first = vi.fn() + const latest = vi.fn() + const { rerender, unmount } = renderHook(({ fn }) => useUnmount(fn), { + initialProps: { fn: first }, + }) + + rerender({ fn: latest }) + unmount() + + expect(first).not.toHaveBeenCalled() + expect(latest).toHaveBeenCalledTimes(1) + }) + + it("should call the committed fn when React drops a render", async () => { + const never = new Promise(() => {}) + const Suspends = () => { + "use no memo" + + use(never) + + return null + } + const committed = vi.fn() + const dropped = vi.fn() + const rendered: Array<() => void> = [] + let setFn: (fn: () => void) => void = () => {} + + const Comp = () => { + "use no memo" + + const [fn, set] = useState<() => void>(() => committed) + setFn = set + rendered.push(fn) + useUnmount(fn) + + return ( + {fn === dropped && } + ) + } + + const { unmount } = render() + + await act(async () => { + startTransition(() => setFn(() => dropped)) + }) + + unmount() + + expect(rendered).toContain(dropped) + expect(dropped).not.toHaveBeenCalled() + expect(committed).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/lib/utils/react/useUnmount.ts b/src/lib/utils/react/useUnmount.ts index 137c5ac..7b10c9c 100644 --- a/src/lib/utils/react/useUnmount.ts +++ b/src/lib/utils/react/useUnmount.ts @@ -1,12 +1,9 @@ -import { useRef } from "react" import { useEffectOnce } from "./useEffectOnce" +import { useLiveRef } from "./useLiveRef" // biome-ignore lint/suspicious/noExplicitAny: Expected any export const useUnmount = (fn: () => any): void => { - const fnRef = useRef(fn) - - // update the ref each render so if it change the newest callback will be invoked - fnRef.current = fn + const fnRef = useLiveRef(fn) useEffectOnce(() => () => fnRef.current()) }