├── .gitignore
├── .swcrc
├── LICENSE
├── README.md
├── __tests__
└── basic-test.js
├── babel.config.js
├── package-lock.json
├── package.json
└── src
├── index.d.ts
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 |
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/swcrc",
3 | "jsc": {
4 | "parser": {
5 | "syntax": "ecmascript",
6 | "jsx": false
7 | },
8 | "target": "es5",
9 | "externalHelpers": false
10 | },
11 | "minify": true
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Cyandev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `useMountEffect`
2 |
3 | [](https://badge.fury.io/js/use-mount-effect)
4 |
5 | A React hook triggered only once on mount.
6 |
7 | ## Background
8 |
9 | > Why does my effect run twice when the component mounts?? It's f\*\*king annoying!
10 |
11 | Well. That is not a bug in React, actually it's by design according to the [doc](https://react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts):
12 |
13 | > When Strict Mode is on, in development, React runs setup and cleanup one extra time before the actual setup.
14 |
15 | There are some workarounds to solve this problem, but people still feel it's a hassle. That's why I made this.
16 |
17 | ## Install
18 |
19 | ```
20 | npm install --save use-mount-effect
21 | ```
22 |
23 | ## Usage
24 |
25 | Basic usage:
26 |
27 | ```js
28 | import { useMountEffect } from "use-mount-effect";
29 |
30 | const MyComponent = () => {
31 | useMountEffect(() => {
32 | // Do something only when the component mounts.
33 | });
34 | return
Blah blah blah
;
35 | };
36 | ```
37 |
38 | With cleanup:
39 |
40 | ```js
41 | import { useMountEffect } from "use-mount-effect";
42 |
43 | const MyComponent = () => {
44 | useMountEffect(() => {
45 | // Do something only when the component mounts.
46 | return () => {
47 | // Do something only when the component unmounts.
48 | };
49 | });
50 | return Blah blah blah
;
51 | };
52 | ```
53 |
54 | ## FAQ
55 |
56 | ### Should I use this hook in production mode?
57 | Actually, you don't need to use this hook in production mode, because it behaves just like `useEffect`. You should really fix your effects if you find your app works incorrectly due to the strict mode, because it implies some logic errors or resource leakage.
58 |
59 | **Always use this hook with caution.**
60 |
61 | ### What's the difference between it and `useEffect`?
62 | The cleanup callback is delayed until the next event loop when you use `useMountEffect`, even in production mode. Be sure to verify that it doesn't affect your code.
63 |
64 | ### Do I really need this?
65 | Just one more reminder. `useEffect` doesn't run twice in production mode. And if there is no user-visible behavior difference between running it once and running it twice, you don't need `useMountEffect`. To learn more, check out [this](https://react.dev/learn/synchronizing-with-effects#sending-analytics).
66 |
67 | ## License
68 |
69 | MIT
70 |
--------------------------------------------------------------------------------
/__tests__/basic-test.js:
--------------------------------------------------------------------------------
1 | import { render, cleanup, waitFor } from "@testing-library/react";
2 |
3 | import { useMountEffect } from "../src/index";
4 |
5 | test("useMountEffect should invoke the callback once on mount", async () => {
6 | const mountCallback = jest.fn();
7 | const unmountCallback = jest.fn();
8 |
9 | const TestComponent = () => {
10 | useMountEffect(() => {
11 | mountCallback();
12 | return () => {
13 | unmountCallback();
14 | };
15 | });
16 |
17 | return null;
18 | };
19 |
20 | render();
21 | await waitFor(() => expect(mountCallback).toHaveBeenCalledTimes(1));
22 |
23 | cleanup();
24 | await waitFor(() => expect(unmountCallback).toHaveBeenCalledTimes(1));
25 |
26 | // Ensure our callback was called exactly once.
27 | expect(mountCallback).toHaveBeenCalledTimes(1);
28 | });
29 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | "@babel/preset-env",
4 | ["@babel/preset-react", { runtime: "automatic" }],
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-mount-effect",
3 | "version": "1.0.0",
4 | "description": "A React hook triggered only once on mount.",
5 | "main": "./dist/index.js",
6 | "module": "./src/index.js",
7 | "types": "./src/index.d.ts",
8 | "scripts": {
9 | "compile": "swc ./src/index.js -o ./dist/index.js",
10 | "test": "jest",
11 | "prepare": "npm run compile"
12 | },
13 | "files": [
14 | "dist/**/*",
15 | "src/**/*"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/unixzii/use-mount-effect.git"
20 | },
21 | "keywords": [
22 | "client",
23 | "componentDidMount",
24 | "react",
25 | "react hooks",
26 | "hooks"
27 | ],
28 | "author": "Cyandev ",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/unixzii/use-mount-effect/issues"
32 | },
33 | "homepage": "https://github.com/unixzii/use-mount-effect#readme",
34 | "devDependencies": {
35 | "@babel/preset-env": "^7.23.8",
36 | "@babel/preset-react": "^7.23.3",
37 | "@swc/cli": "^0.1.63",
38 | "@swc/core": "^1.3.102",
39 | "@testing-library/react": "^14.1.2",
40 | "babel-jest": "^29.7.0",
41 | "jest": "^29.7.0",
42 | "jest-environment-jsdom": "^29.7.0",
43 | "react": "^18.2.0",
44 | "react-dom": "^18.2.0"
45 | },
46 | "peerDependencies": {
47 | "react": ">=16.0.0"
48 | },
49 | "jest": {
50 | "verbose": true,
51 | "testEnvironment": "jsdom"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | type Destructor = () => void;
2 | type EffectCallback = () => void | Destructor;
3 | type UseMountEffect = (effect: EffectCallback) => void;
4 |
5 | /**
6 | * It works similarly to `useEffect`, but it fires only once after the component is mounted.
7 | * Use this to register some operations that should only perform when the component is mounted, regardless
8 | * of whether strict mode is enabled.
9 | *
10 | * @param effect Imperative function that can return a cleanup function
11 | */
12 | export const useMountEffect: UseMountEffect;
13 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | const scheduleCallback = (() => {
4 | if (typeof window === "undefined" || !window.MessageChannel) {
5 | // Fallback implementation in non-browser environments.
6 | return (cb) => {
7 | setTimeout(cb, 0);
8 | };
9 | }
10 |
11 | const channel = new MessageChannel();
12 | let head = [null, null];
13 | let tail = head;
14 | channel.port1.onmessage = () => {
15 | const cb = head[0];
16 | if (cb) {
17 | head = head[1];
18 | cb();
19 | }
20 | };
21 | return (cb) => {
22 | const nextNode = [null, null];
23 | tail[0] = cb;
24 | tail[1] = nextNode;
25 | tail = nextNode;
26 | channel.port2.postMessage(0);
27 | };
28 | })();
29 |
30 | export function useMountEffect(cb) {
31 | const state = useRef({
32 | mounted: false,
33 | cleanup: null,
34 | scheduledCleanups: [],
35 | });
36 |
37 | useEffect(() => {
38 | const invokeCleanup = () => {
39 | const cleanup = state.current.cleanup;
40 | if (!cleanup) {
41 | return;
42 | }
43 |
44 | const cancellationToken = { value: false };
45 | scheduleCallback(() => {
46 | if (cancellationToken.value) {
47 | return;
48 | }
49 |
50 | cleanup();
51 | });
52 |
53 | state.current.scheduledCleanups.push(cancellationToken);
54 | };
55 |
56 | if (state.current.mounted) {
57 | // Ignore the second invocation in DEV but cancel the
58 | // scheduled cleanups.
59 | state.current.scheduledCleanups.forEach((t) => {
60 | t.value = true;
61 | });
62 | state.current.scheduledCleanups = [];
63 | return invokeCleanup;
64 | }
65 |
66 | state.current.mounted = true;
67 | state.current.cleanup = cb();
68 |
69 | return invokeCleanup;
70 | }, []);
71 | }
72 |
--------------------------------------------------------------------------------