Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update useOnMountEffect to pass isMountedRef to callback #2413

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-shoes-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-core": minor
---

Update useOnMountEffect to pass isMountedRef to callback
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {renderHook} from "@testing-library/react";
import * as React from "react";
import {renderHook, waitFor} from "@testing-library/react";

import {useOnMountEffect} from "../use-on-mount-effect";

Expand Down Expand Up @@ -27,4 +28,90 @@ describe("#useOnMountEffect", () => {
// Assert
expect(cleanup).toHaveBeenCalled();
});

it("should pass {current: true} as isMountedRef on initial render", () => {
// Arrange
const callback = jest.fn();

// Act
renderHook(() => useOnMountEffect(callback));

// Assert
expect(callback).toHaveBeenCalledWith({current: true});
});

it("should pass {current: false} as isMountedRef after being unmounted", () => {
// Arrange
const callback = jest.fn();

// Act
const {unmount} = renderHook(() => useOnMountEffect(callback));
unmount();

// Assert
expect(callback).toHaveBeenCalledWith({current: false});
});

describe("async", () => {
it("should pass {current: true} while mounted", async () => {
// Arrange
const wait = async (duration: number) => {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
};
let foo = false;
const callback = (
isMountedRef: React.MutableRefObject<boolean>,
) => {
const action = async () => {
await wait(100);
if (isMountedRef.current) {
foo = true;
}
};

action();
};

// Act
renderHook(() => useOnMountEffect(callback));

// Assert
await waitFor(() => {
expect(foo).toEqual(true);
});
});

it("should pass {current: false} after being unmounted", async () => {
// Arrange
const wait = async (duration: number) => {
return new Promise((resolve) => {
setTimeout(resolve, duration);
});
};
let foo = false;
const callback = (
isMountedRef: React.MutableRefObject<boolean>,
) => {
const action = async () => {
await wait(100);
if (isMountedRef.current) {
foo = true;
}
};

action();
};

// Act
const {unmount} = renderHook(() => useOnMountEffect(callback));
unmount();

// Assert
await waitFor(() => {
expect(foo).toEqual(false);
});
});
});
});
42 changes: 38 additions & 4 deletions packages/wonder-blocks-core/src/hooks/use-on-mount-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
*
* If the callback returns a cleanup function, it will be called when the component is unmounted.
*
* @param {() => (void | (() => void))} callback function that forces the component to update.
* @param {(isMountedRef: React.MutableRefObject<boolean>) => void | (() => void)} callback function that forces the component to update.
*
* The following code snippets are equivalent
* ```
Expand All @@ -27,8 +27,42 @@ import * as React from "react";
* }, []);
*
* If you only need to do something on mount, don't return a cleanup function from `callback`.
*
* If your callback is async, use the `isMountedRef` ref that's passed to the callback to ensure
* that the component using `useOnMountEffect` hasn't been unmounted, e.g.
*
* ```
* const MyComponent = () => {
* const [foo, setFoo] = React.useState("");
* useOnMountEffect((isMountedRef) => {
* const action = async () => {
* const res = await fetch("/foo");
* const text = res.text();
* if (isMountedRef.current) {
* setFoo(text);
* }
* }
*
* action();
* });
*
* return foo !== "" ? <h1>Loading...</h1> : <h1>{foo}</h1>;
* }
*/
export const useOnMountEffect = (callback: () => void | (() => void)): void => {
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(callback, []);
export const useOnMountEffect = (
callback: (
isMountedRef: React.MutableRefObject<boolean>,
) => void | (() => void),
): void => {
const isMountedRef = React.useRef(true);

React.useEffect(() => {
const cleanup = callback(isMountedRef);

return () => {
cleanup?.();
isMountedRef.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
Loading