
Have you ever filled out a form only to accidentally hit refresh or close the browser tab losing everything in the process?
This small frustration can become a big issue in apps like:
- Multi-step forms
- Chat drafts
- Profile or ticket submission pages
- In-progress document editors
Let’s build something that politely tells the browser: Hey, not so fast — I’ve got unsaved work here! 👀
✨ What We’ll Build
✅ A React form that tracks unsaved data
✅ A confirmation dialog on refresh or close
✅ A unit test to verify the behaviour
✨ Step 1: Create a React Form
Let’s build a form that prompts the user when they try to refresh or close the tab with unsaved changes :
// Form Component
const Form = () => {
const [inputValue, setInputValue] = useState ();
const [formChanged, setFormChanged] = useState ( false );
useEffect ( () => {
const handleBeforeUnload = ( event ) => {
(formChanged) {
event. preventDefault ();
event. returnValue = ; } };
window . addEventListener ( 'beforeunload' , handleBeforeUnload);
return () => window . removeEventListener ( 'beforeunload' , handleBeforeUnload); }, [formChanged]);
return ( <form> <label> Type something: <input type="text" value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setFormChanged(true); }} /> </label> </form> ); };
export default Form ;
const Form = () => {
const [inputValue, setInputValue] = useState ();
const [formChanged, setFormChanged] = useState ( false );
useEffect ( () => {
const handleBeforeUnload = ( event ) => {
(formChanged) {
event. preventDefault ();
event. returnValue = ; } };
window . addEventListener ( 'beforeunload' , handleBeforeUnload);
return () => window . removeEventListener ( 'beforeunload' , handleBeforeUnload); }, [formChanged]);
return ( <form> <label> Type something: <input type="text" value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
setFormChanged(true); }} /> </label> </form> ); };
export default Form ;
💡 Try it out in a React project. Type in the input and then refresh or close the tab, you’ll see a native confirmation dialog pop up.⚠️ Common Errors You Might Hit (And How to Fix Them)
When you implement this in a real-world codebase, especially with TypeScript and ESLint, you might see a couple of common warnings or errors.
1. no-param-reassign: Assignment to function parameter 'event'
The error:
Assignment to property of
function parameter 'event'
function parameter 'event'
This comes from the line:
event.returnValue =
✅ Why it happens
Most ESLint configs don’t allow changing function parameters directly, even though we need to modify event.returnValue as it is required for showing our confirmation dialog.
🔧 How to fix it
Tell ESLint to ignore this line explicitly:
// eslint-disable-next-line no-param-reassign event .returnValue =
event.returnValue = '';is typically used in certain browser APIs (likebeforeunloadevent handlers) to prevent the browser's default behaviour like a page refresh or close.- The comment
// eslint-disable-next-line no-param-reassignis used to tell ESLint to ignore the rule for the next line of code. This is safe here because it’s a special browser API requirement.

2. label-has-associated-control Error
The error:
form label must be associated with control
The issue:
If we use a <label> like this:
<label> Type something : <input ... /> </label>
Some ESLint rules need us to explicitly associate the label with the input using htmlFor.
✅ Fix: Add htmlFor and id
<label htmlFor= "input" > Type something :</label> <input id="input" type="text" value={inputValue}
onChange={handleOnChange} />
onChange={handleOnChange} />
This improves accessibility and satisfies ESLint.
Updated Example Code:
import React , {
useEffect, useState }
from 'react' ;
const Form = () => {
const [inputValue, setInputValue] = useState ();
const [formChanged, setFormChanged] = useState ( false );
useEffect ( () => {
const handleBeforeUnload = ( event: any ) => {
(formChanged) {
event. preventDefault (); // eslint-disable-next-line no-param-reassign event. returnValue = ; } };
window . addEventListener ( 'beforeunload' , handleBeforeUnload);
return () => window . removeEventListener ( 'beforeunload' , handleBeforeUnload); }, [formChanged]);
const handleOnChange = ( event: any ) => {
setInputValue (event. target . value );
setFormChanged ( true ); };
return ( <form> <label htmlFor="input">Type something:</label> <input id="input" type="text" value={inputValue}
onChange={handleOnChange} /> </form> ); };
export default Form ;
useEffect, useState }
from 'react' ;
const Form = () => {
const [inputValue, setInputValue] = useState ();
const [formChanged, setFormChanged] = useState ( false );
useEffect ( () => {
const handleBeforeUnload = ( event: any ) => {
(formChanged) {
event. preventDefault (); // eslint-disable-next-line no-param-reassign event. returnValue = ; } };
window . addEventListener ( 'beforeunload' , handleBeforeUnload);
return () => window . removeEventListener ( 'beforeunload' , handleBeforeUnload); }, [formChanged]);
const handleOnChange = ( event: any ) => {
setInputValue (event. target . value );
setFormChanged ( true ); };
return ( <form> <label htmlFor="input">Type something:</label> <input id="input" type="text" value={inputValue}
onChange={handleOnChange} /> </form> ); };
export default Form ;
🧪 Step 2: Writing a Unit Test
We’ll now test that the beforeunload event listener is correctly attached.
// Test
for Browser Confirmation window
import {
render }
from '@testing-library/react' ;
import Form
from './Form' ;
describe ( 'Form' , () => {
( 'should display confirmation dialog
for unsaved changes
on window refresh' , async () => {
render (<Form />);
const event = new Event ( 'beforeunload' , {
cancelable : true });
Object . defineProperty (event, 'returnValue' , {
writable : true , value : });
window . dispatchEvent (event);
await waitFor ( ()=> {
expect (event. defaultPrevented ). toBe ( true )}); }); });
for Browser Confirmation window
import {
render }
from '@testing-library/react' ;
import Form
from './Form' ;
describe ( 'Form' , () => {
( 'should display confirmation dialog
for unsaved changes
on window refresh' , async () => {
render (<Form />);
const event = new Event ( 'beforeunload' , {
cancelable : true });
Object . defineProperty (event, 'returnValue' , {
writable : true , value : });
window . dispatchEvent (event);
await waitFor ( ()=> {
expect (event. defaultPrevented ). toBe ( true )}); }); });
✅ This confirms the event listener is added. You can further extend the test to simulate beforeunload and assert that preventDefault() is triggered when the form input has been changed.⚠️ Known Limitations and things to consider:
Here are a few important caveats from real-world testing and documentation that need to be considered before digging into implementing this:
Custom messages in dialogs are ignored: Browsers no longer show your custom text for security reasons.
Dialogs may not show at all: Unless the user has interacted with the page (e.g: clicked or typed), most browsers will suppress the dialog.
Don’t overuse it: Show the confirmation only when necessary (e.g: form has unsaved changes) to avoid annoying users.
Memory leaks: Always clean up the event listener using the useEffect return function.
✅ Wrapping Up
We just implemented a native browser confirmation dialog to prevent accidental refresh or tab close when there’s unsaved input. To recap, we:
🧩 Built a short React form with changed input state detection
🔒 Added a beforeunload listener
🧪 Wrote a unit test
This is especially valuable in productivity tools, chat apps, or any multi-step process. Add it where needed but be user-friendly.
References:
- https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
- https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
- Stackoverflow discussion:
Show A Warning If Page is Closed or Refreshed in ReactJS
My problem is that I need the user to confirm if he wants to continue to close or refresh the page. If he press No, it…
stackoverflow.com
If you have any questions or want to connect, I’d love to hear from you! You can find me on : LinkedIn Github Happy coding!✌️


