├── .husky
├── .gitignore
├── pre-commit
└── commit-msg
├── .prettierignore
├── prettier.config.js
├── .gitignore
├── src
├── index.js
├── Beforeunload.js
├── __tests__
│ ├── Beforeunload.test.js
│ └── useBeforeunload.test.js
└── useBeforeunload.js
├── commitlint.config.js
├── lint-staged.config.js
├── vitest.setup.js
├── vite.config.js
├── eslint.config.js
├── LICENSE
├── package.json
├── README.md
└── CHANGELOG.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | coverage
2 | dist
3 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx --no -- lint-staged
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit $1
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | dist
3 | node_modules
4 | package-lock.json
5 | yarn.lock
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * from "./Beforeunload";
2 | export * from "./useBeforeunload";
3 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | extends: ["@commitlint/config-conventional"],
3 | };
4 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "*": ["prettier --write --ignore-unknown"],
3 | "*.js": ["eslint --fix"],
4 | };
5 |
--------------------------------------------------------------------------------
/vitest.setup.js:
--------------------------------------------------------------------------------
1 | import { cleanup } from "@testing-library/react";
2 | import { afterEach } from "vitest";
3 |
4 | afterEach(() => {
5 | cleanup();
6 | });
7 |
--------------------------------------------------------------------------------
/src/Beforeunload.js:
--------------------------------------------------------------------------------
1 | import { useBeforeunload } from "./useBeforeunload";
2 |
3 | export const Beforeunload = ({ children = null, onBeforeunload }) => {
4 | useBeforeunload(onBeforeunload);
5 | return children;
6 | };
7 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from "node:path";
2 | import { defineConfig } from "vite";
3 |
4 | export default defineConfig({
5 | build: {
6 | lib: {
7 | entry: resolve(__dirname, "src/index.js"),
8 | formats: ["es", "cjs"],
9 | },
10 | rollupOptions: {
11 | external: ["react"],
12 | },
13 | sourcemap: true,
14 | },
15 | test: {
16 | environment: "jsdom",
17 | setupFiles: ["@testing-library/jest-dom/vitest", "./vitest.setup.js"],
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/__tests__/Beforeunload.test.js:
--------------------------------------------------------------------------------
1 | import { expect, test, vi } from "vitest";
2 | import { createElement } from "react";
3 | import { act, render } from "@testing-library/react";
4 | import { Beforeunload } from "..";
5 |
6 | test("passes onBeforeunload prop to useBeforeunload hook", () => {
7 | const handler = vi.fn();
8 | render(createElement(Beforeunload, { onBeforeunload: handler }));
9 | const event = new Event("beforeunload", { cancelable: true });
10 | act(() => {
11 | window.dispatchEvent(event);
12 | });
13 | expect(handler).toHaveBeenCalledWith(event);
14 | });
15 |
16 | test("renders children", () => {
17 | const { container } = render(
18 | createElement(
19 | Beforeunload,
20 | { onBeforeunload: () => {} },
21 | "Hello ",
22 | createElement("strong", null, "World!"),
23 | ),
24 | );
25 | expect(container).toContainHTML("Hello World!");
26 | });
27 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import pluginVitest from "@vitest/eslint-plugin";
3 | import { defineConfig, globalIgnores } from "eslint/config";
4 | import configPrettier from "eslint-config-prettier/flat";
5 | import pluginJsdoc from "eslint-plugin-jsdoc";
6 | import pluginReactHooks from "eslint-plugin-react-hooks";
7 | import globals from "globals";
8 |
9 | export default defineConfig([
10 | globalIgnores(["coverage", "dist"]),
11 | js.configs.recommended,
12 | pluginJsdoc.configs["flat/recommended"],
13 | pluginReactHooks.configs.flat["recommended-latest"],
14 | configPrettier,
15 | {
16 | files: ["**/*.js"],
17 | languageOptions: {
18 | globals: { ...globals.browser, ...globals.node },
19 | ecmaVersion: "latest",
20 | sourceType: "module",
21 | },
22 | },
23 | {
24 | files: ["**/*.test.js", "**/__tests__/**/*.js"],
25 | extends: [pluginVitest.configs.recommended],
26 | },
27 | ]);
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2025 Alie Buck
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/useBeforeunload.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | /**
4 | * @callback BeforeUnloadHandler
5 | * @param {BeforeUnloadEvent} event
6 | * @returns {boolean|string|undefined} Truthy values trigger the confirmation dialog.
7 | */
8 |
9 | /**
10 | * React hook that listens to `beforeunload` window event.
11 | * @function
12 | * @param {BeforeUnloadHandler|false|null|undefined} handler - Event listener callback:
13 | * Called on `beforeunload` window event. It activates a confirmation dialog
14 | * when `event.preventDefault()` is called or a truthy value is returned.
15 | */
16 | export const useBeforeunload = (handler) => {
17 | const handlerRef = useRef(handler);
18 | useEffect(() => {
19 | handlerRef.current = handler;
20 | });
21 |
22 | const enabled = typeof handler === "function";
23 |
24 | useEffect(() => {
25 | if (enabled) {
26 | const listener = (event) => {
27 | const returnValue = handlerRef.current(event);
28 | /** @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */
29 | if (returnValue || typeof returnValue === "string") {
30 | event.preventDefault();
31 | event.returnValue = returnValue;
32 | } else if (event.defaultPrevented) {
33 | event.returnValue = true;
34 | }
35 | };
36 | window.addEventListener("beforeunload", listener);
37 | return () => window.removeEventListener("beforeunload", listener);
38 | }
39 | }, [enabled]);
40 | };
41 |
--------------------------------------------------------------------------------
/src/__tests__/useBeforeunload.test.js:
--------------------------------------------------------------------------------
1 | import { expect, test, vi } from "vitest";
2 | import { act, renderHook } from "@testing-library/react";
3 | import { useBeforeunload } from "..";
4 |
5 | const createBeforeunloadEvent = () =>
6 | new Event("beforeunload", { cancelable: true });
7 |
8 | const dispatchWindowEvent = (event) =>
9 | act(() => {
10 | window.dispatchEvent(event);
11 | });
12 |
13 | const renderUseBeforeunloadHook = (handler) =>
14 | renderHook(() => useBeforeunload(handler));
15 |
16 | test("handler function is called when beforeunload event is fired", () => {
17 | const handler = vi.fn();
18 | renderUseBeforeunloadHook(handler);
19 | const event = createBeforeunloadEvent();
20 | dispatchWindowEvent(event);
21 | expect(handler).toHaveBeenCalledWith(event);
22 | });
23 |
24 | test("returnValue on event is set when preventDefault is called", () => {
25 | renderUseBeforeunloadHook((event) => {
26 | event.preventDefault();
27 | });
28 | const event = createBeforeunloadEvent();
29 | // jsdom currently doesn't have `BeforeUnloadEvent` implemented, so we're just
30 | // ensuring `returnValue` is set on `event`
31 | const set = vi.fn();
32 | Object.defineProperty(event, "returnValue", { set });
33 | dispatchWindowEvent(event);
34 | expect(set).toHaveBeenCalledWith(true);
35 | });
36 |
37 | test("returnValue on event is set when a string is returned by handler", () => {
38 | renderUseBeforeunloadHook(() => "goodbye");
39 | const event = createBeforeunloadEvent();
40 | // jsdom currently doesn't have `BeforeUnloadEvent` implemented, so we're just
41 | // ensuring `returnValue` is set on `event`
42 | const set = vi.fn();
43 | Object.defineProperty(event, "returnValue", { set });
44 | dispatchWindowEvent(event);
45 | expect(set).toHaveBeenCalledWith("goodbye");
46 | });
47 |
48 | test("doesn’t throw if handler is not a function", () => {
49 | expect(() => {
50 | renderUseBeforeunloadHook(true);
51 | renderUseBeforeunloadHook(false);
52 | renderUseBeforeunloadHook(null);
53 | renderUseBeforeunloadHook(undefined);
54 | renderUseBeforeunloadHook("");
55 | renderUseBeforeunloadHook({});
56 | renderUseBeforeunloadHook(0);
57 | dispatchWindowEvent(createBeforeunloadEvent());
58 | }).not.toThrow();
59 | });
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-beforeunload",
3 | "version": "2.7.0",
4 | "description": "React component and hook which listens to the beforeunload window event.",
5 | "keywords": [
6 | "beforeunload",
7 | "component",
8 | "event",
9 | "hook",
10 | "onbeforeunload",
11 | "react",
12 | "unload",
13 | "window"
14 | ],
15 | "homepage": "https://github.com/aliebuck/react-beforeunload#readme",
16 | "bugs": {
17 | "url": "https://github.com/aliebuck/react-beforeunload/issues"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/aliebuck/react-beforeunload.git"
22 | },
23 | "license": "MIT",
24 | "author": "Alie Buck <830470+aliebuck@users.noreply.github.com>",
25 | "sideEffects": false,
26 | "type": "module",
27 | "exports": {
28 | ".": {
29 | "import": "./dist/react-beforeunload.js",
30 | "require": "./dist/react-beforeunload.cjs"
31 | }
32 | },
33 | "main": "./dist/react-beforeunload.cjs",
34 | "module": "./dist/react-beforeunload.js",
35 | "files": [
36 | "dist"
37 | ],
38 | "scripts": {
39 | "build": "vite build",
40 | "coverage": "vitest run --coverage",
41 | "format": "prettier --write .",
42 | "lint": "eslint .",
43 | "prepare": "npm run build && husky",
44 | "test": "vitest",
45 | "preversion": "npm run lint && npm run coverage"
46 | },
47 | "devDependencies": {
48 | "@commitlint/cli": "^20.1.0",
49 | "@commitlint/config-conventional": "^20.0.0",
50 | "@eslint/js": "^9.39.1",
51 | "@testing-library/dom": "^10.4.1",
52 | "@testing-library/jest-dom": "^6.9.1",
53 | "@testing-library/react": "^16.3.0",
54 | "@vitest/coverage-v8": "^4.0.15",
55 | "@vitest/eslint-plugin": "^1.5.1",
56 | "eslint": "^9.39.1",
57 | "eslint-config-prettier": "^10.1.8",
58 | "eslint-plugin-jsdoc": "^61.4.1",
59 | "eslint-plugin-prettier": "^5.5.4",
60 | "eslint-plugin-react-hooks": "^7.0.1",
61 | "globals": "^16.5.0",
62 | "husky": "^9.1.7",
63 | "jsdom": "^27.2.0",
64 | "lint-staged": "^16.2.7",
65 | "prettier": "^3.7.4",
66 | "react": "^19.0.0",
67 | "react-dom": "^19.0.0",
68 | "vite": "^7.2.6",
69 | "vitest": "^4.0.15"
70 | },
71 | "peerDependencies": {
72 | "react": ">=16.8.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-beforeunload
2 |
3 | Listen to the [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) window event in React.
4 |
5 | ## Usage
6 |
7 | ### `useBeforeunload` Hook (recommended)
8 |
9 | ```jsx
10 | useBeforeunload(handler);
11 | ```
12 |
13 | #### Parameters
14 |
15 | - `handler` optional function to be called with `BeforeUnloadEvent` when `beforeunload` event is fired.
16 | Passing a non-function value will disable the event listener.
17 |
18 | #### Example
19 |
20 | ##### Simple
21 |
22 | ```jsx
23 | import { useBeforeunload } from "react-beforeunload";
24 |
25 | const Example = (props) => {
26 | useBeforeunload((event) => event.preventDefault());
27 | ...
28 | };
29 | ```
30 |
31 | ##### Conditional
32 |
33 | ```jsx
34 | import { useBeforeunload } from "react-beforeunload";
35 |
36 | const Example = (props) => {
37 | const [value, setValue] = useState("");
38 |
39 | useBeforeunload(value !== "" ? (event) => event.preventDefault() : null);
40 | // or
41 | useBeforeunload(value !== "" && () => true);
42 |
43 | ...
44 | };
45 | ```
46 |
47 | ### `Beforeunload` Component
48 |
49 | ```jsx
50 |
51 | ```
52 |
53 | #### Props
54 |
55 | - `onBeforeunload` function to be called with `BeforeUnloadEvent` when `beforeunload` event is fired.
56 |
57 | #### Example
58 |
59 | ```jsx
60 | import { Beforeunload } from "react-beforeunload";
61 |
62 | class Example extends React.Component {
63 | state = { value: "" };
64 |
65 | render() {
66 | return (
67 | <>
68 | {this.state.value !== "" && (
69 | event.preventDefault()} />
70 | )}
71 | this.setState({ value: event.target.value })}
73 | value={this.state.value}
74 | />
75 | >
76 | );
77 | }
78 | }
79 | ```
80 |
81 | :information_source: The `Beforeunload` component will render any children passed as-is, so it can be used as a wrapper component:
82 |
83 | ```jsx
84 |
85 |
86 |
87 | ```
88 |
89 | ## Custom message support
90 |
91 | > :warning: Some browsers used to display the returned string in the confirmation dialog, enabling the event handler to display a custom message to the user. However, this is deprecated and no longer supported in most browsers.
92 |
93 | [Source](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event)
94 |
95 | To display a custom message in the triggered dialog box, return a string in the passed event handler function.
96 |
97 | With `useBeforeunload` hook:
98 |
99 | ```jsx
100 | useBeforeunload(() => "You’ll lose your data!");
101 | ```
102 |
103 | With `Beforeunload` component:
104 |
105 | ```jsx
106 | "You’ll lose your data!"} />
107 | ```
108 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v2.7.0 - 2025-12-06
4 |
5 | ### Changed
6 |
7 | - Changed default `event.returnValue` fallback from `''` to `true` in `beforeunload` handler. (Fixes [#33](https://github.com/aliebuck/react-beforeunload/issues/33))
8 | - Updated [react](https://www.npmjs.com/package/react) peer dependency to support v19+. (Fixes [#34](https://github.com/aliebuck/react-beforeunload/issues/34))
9 |
10 | ### Removed
11 |
12 | - Removed deprecated `return`-based activation from `beforeunload` handler.
13 |
14 | ## v2.6.0 - 2023-06-16
15 |
16 | ### Added
17 |
18 | - Added conditional listening in `useBeforeunload` hook. (Fixes [#9](https://github.com/aliebuck/react-beforeunload/issues/9))
19 | - Added `sideEffects` property in [package.json](./package.json).
20 |
21 | ### Changed
22 |
23 | - Updated `handler` parameter in `useBeforeunload` hook to be optional.
24 |
25 | ### Removed
26 |
27 | - Removed type checking.
28 | - Removed [prop-types](https://www.npmjs.com/package/prop-types) dependency.
29 | - Removed [tiny-invariant](https://www.npmjs.com/package/tiny-invariant) dependency.
30 |
31 | ## v2.5.3 - 2022-04-12
32 |
33 | ### Changed
34 |
35 | - Updated [prop-types](https://www.npmjs.com/package/prop-types) dependency to v15.8.1.
36 | - Updated [tiny-invariant](https://www.npmjs.com/package/tiny-invariant) dependency to v1.2.0.
37 | - Updated [react](https://www.npmjs.com/package/react) peer dependency to support v18.
38 |
39 | ## v2.5.2 - 2021-10-03
40 |
41 | ### Fixed
42 |
43 | - Fixed legacy dialog activation using `return "string";` method. (Fixes [#27](https://github.com/aliebuck/react-beforeunload/issues/27))
44 |
45 | ## v2.5.1 - 2021-05-02
46 |
47 | ### Removed
48 |
49 | - Removed [use-latest](https://www.npmjs.com/package/use-latest) dependency.
50 |
51 | ## v2.5.0 - 2021-04-25
52 |
53 | ### Added
54 |
55 | - Added [tiny-invariant](https://www.npmjs.com/package/tiny-invariant) dependency.
56 |
57 | ### Changed
58 |
59 | - Changed type checking in `useBeforeunload` hook to use `invariant` function.
60 | - Updated `Beforeunload.propTypes` to only be defined in non-production environments.
61 | - Updated internal event handler to set `event.returnValue` less times.
62 |
63 | ## v2.4.0 - 2020-11-08
64 |
65 | ### Added
66 |
67 | - Added source maps to build output.
68 |
69 | ### Changed
70 |
71 | - Updated [use-latest](https://www.npmjs.com/package/use-latest) dependency to v1.2.0.
72 | - Updated [react](https://www.npmjs.com/package/react) peer dependency to support v17.
73 |
74 | ## v2.3.0 - 2020-10-26
75 |
76 | ### Changed
77 |
78 | - Improved type-checking.
79 | - Updated `handler` parameter of `useBeforeunload` hook to allow [nullish values](https://developer.mozilla.org/en-US/docs/Glossary/Nullish).
80 |
81 | ### Removed
82 |
83 | - Removed `defaultProps` in favour of default values in object destructuring.
84 |
85 | ## v2.2.4 - 2020-09-02
86 |
87 | ### Changed
88 |
89 | - Updated `Beforeunload.propTypes` to only be defined in non-production environments.
90 |
91 | ## v2.2.3 - 2020-08-28
92 |
93 | ### Removed
94 |
95 | - Removed redundant type-check.
96 |
97 | ## v2.2.2 - 2020-07-09
98 |
99 | ### Changed
100 |
101 | - Used [use-latest](https://www.npmjs.com/package/use-latest) for handling refs in `useBeforeunload` hook.
102 |
103 | ## v2.2.1 - 2020-05-20
104 |
105 | ### Changed
106 |
107 | - Enabled loose mode on '@babel/preset-env' to reduce build output.
108 |
109 | ## v2.2.0 - 2020-04-27
110 |
111 | ### Added
112 |
113 | - Added ES Module build.
114 | - Added `defaultProps` to `Beforeunload` component.
115 |
116 | ## v2.1.0 - 2019-06-23
117 |
118 | ### Added
119 |
120 | - Added type-checking to `useBeforeunload` hook.
121 |
122 | ## v2.0.1 - 2019-06-20
123 |
124 | ### Added
125 |
126 | - Added `Event.preventDefault()` workaround for Chromium browsers.
127 |
128 | ### Removed
129 |
130 | - Removed `default` export.
131 |
132 | ## v2.0.0 - 2019-06-02
133 |
134 | ### Added
135 |
136 | - Added `useBeforeunload` hook.
137 |
138 | ### Changed
139 |
140 | - **BREAKING** Requires [react](https://www.npmjs.com/package/react) peer dependency to be v16.8.0 or newer.
141 | - **BREAKING** `Beforeunload` is now a named export.
142 | - Changed `Beforeunload` component to be functional and use hooks internally.
143 |
144 | ## v1.1.1 - 2019-06-02
145 |
146 | ### Fixed
147 |
148 | - Fixed failing builds due to missing Babel plugin.
149 |
150 | ## v1.1.0 - 2019-06-02
151 |
152 | ### Changed
153 |
154 | - Builds are now done with [Rollup](http://rollupjs.org).
155 |
156 | ## v1.0.4 - 2017-08-21
157 |
158 | ### Changed
159 |
160 | - Updated [react](https://www.npmjs.com/package/react) peer dependency to support React 16.
161 |
162 | ## v1.0.3 - 2017-07-28
163 |
164 | ### Fixed
165 |
166 | - Fixed rendering when no `children` are set.
167 |
168 | ## v1.0.2 - 2017-07-27
169 |
170 | ### Fixed
171 |
172 | - Fixed publishing to NPM registry.
173 |
174 | ## v1.0.1 - 2017-07-27
175 |
176 | ### Fixed
177 |
178 | - Fixed wrong class being exported.
179 |
180 | ## v1.0.0 - 2017-07-15
181 |
182 | Initial public version! :tada:
183 |
--------------------------------------------------------------------------------