├── .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 | --------------------------------------------------------------------------------