Preventing Data Loss: Implementing Unsaved Changes Alert in React App ๐ฏ
Have you ever spent a good chunk of time filling out a lengthy form with lots of fields, and then you go to another tab to verify some of the details needed to fill the next field, but while returning back to previous tab, you accidentally click the close button!
We've all been there, the frustration bubbling as hours of work vanish in a digital blink. But fear not, fellow developers, for we have the power to prevent such data loss nightmares in our React applications!
Why Unsaved Changes Alerts? A Balancing Act ๐ฏ
In the fast-paced world of web applications, ensuring a seamless user experience is paramount. Blocking a user from navigating is a bit of an anti-pattern. While a well-timed "Are you sure you want to leave?" message can be a lifesaver, it's important to use this mechanism judiciously. Forcing users to stay on a page can be disruptive to their browsing experience. Think of it as a digital lifeguard โ there to prevent a mishap but not to become an overly-protective helicopter parent. So, when does an unsaved changes alert become necessary?
Long Forms: Imagine a complex multi-step form for loan applications or detailed customer profiles. Losing that data can be a major pain point for both users and businesses.
Data Integrity: Some operations require specific data sequences or dependencies. Leaving a partially completed form could lead to inconsistencies or incomplete records in your database.
Prerequisites ๐
To follow along, you'll need a basic understanding of React, React Router. Additionally, familiarity with JavaScript and JSX syntax will be beneficial. If you're new to React, don't worry โ there are plenty of fantastic resources available online to get you started!
The Hero: The useBlocker
Hook ๐
React Router v6 introduces the useBlocker
hook, a powerful tool for intercepting navigation attempts. We'll combine this with the browser's native beforeunload
event to create a robust unsaved changes detection system.
Make sure not to mix up the beforeunload
event with the deprecated unload
event.
Building Our Unsaved Changes Alert (Code Time!) ๐ป
Let's create a simple form to illustrate this concept. Imagine a user filling out a contact form:
import React, { useState, useEffect } from "react";
import { useNavigate, useBlocker } from "react-router-dom";
function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [unSavedChanges, setUnSavedChanges] = useState(false);
const navigate = useNavigate();
const handleChange = (event) => {
setFormData({ ...formData, [event.target.name]: event.target.value });
!unSavedChanges && setUnSavedChanges(true);
};
const handleSubmit = (event) => {
event.preventDefault();
setUnSavedChanges(false);
console.log("Form submitted:", formData);
setFormData({ name: "", email: "", message: "" });
navigate("/");
};
return (
<form onSubmit={handleSubmit} className="mt_1">
<div>
<label htmlFor="name">Name: </label>
<input
type="text"
id="name"
name="name"
value={formData?.name}
onChange={handleChange}
className="mb_1"
/>
</div>
<div>
<label htmlFor="email">Email: </label>
<input
type="email"
id="email"
name="email"
value={formData?.email}
onChange={handleChange}
className="mb_1"
/>
</div>
<div>
<label htmlFor="message">Message: </label>
<textarea
id="message"
name="message"
value={formData?.message}
onChange={handleChange}
/>
</div>
<button type="submit" className="mt_1">
Submit
</button>
</form>
);
}
export default ContactForm;
Protecting Our Precious Data (Hook & Event Combo) ๐ช
Now, let's add the magic that prevents accidental data loss:
import React, { useState, useEffect } from "react";
import { useNavigate, useBlocker } from "react-router-dom";
function ContactForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const [unSavedChanges, setUnSavedChanges] = useState(false);
const [navigateConfirmModal, setNavigateConfirmModal] = useState(false);
const navigate = useNavigate();
const handleChange = (event) => {
setFormData({ ...formData, [event.target.name]: event.target.value });
!unSavedChanges && setUnSavedChanges(true);
};
const handleSubmit = (event) => {
event.preventDefault();
setUnSavedChanges(false);
console.log("Form submitted:", formData);
setFormData({ name: "", email: "", message: "" });
navigate("/");
};
useEffect(() => {
const handleBeforeUnload = (e) => {
if (unSavedChanges) {
e.preventDefault();
e.returnValue = ""; // required for chrome
return true; // Return a truthy value
}
return null; // Allow navigation if no conditions met
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [unSavedChanges]);
const shouldBlockNavigation = () => {
if (unSavedChanges) {
setNavigateConfirmModal(true);
return "Are you sure you want to leave? You may lose unsaved changes.";
}
return null; // Allow navigation
};
let blocker = useBlocker(shouldBlockNavigation);
return (
<form onSubmit={handleSubmit} className="mt_1">
///// Existing Form Code /////
{navigateConfirmModal && (
<div className='modal'>
<p>Are you sure you want to leave? You may lose unsaved changes</p>
<button className='mr_1' onClick={()=>{setNavigateConfirmModal(false)}}>Cancel</button>
<button onClick={()=>{blocker?.proceed()}}>Proceed</button>
</div>
)}
</form>
);
}
export default ContactForm;
Here's the codeSandBox link with complete implementation.
Explanation ๐
beforeunload
Event: Inside theuseEffect
hook, we register thebeforeunload
event, it handles the cases when there are unsaved changes, and user navigates away through browser actions like back button, reload tab, or close tab etc.useBlocker
hook: For handling the navigation in case user navigates through internal routing (for eg: clicking on "Home" button), when there are unsaved changes. (Please make sure to use latest version of React Router Dom i.e. v6.22+ to have a stable release of useBlocker hook)Whenever
beforeunload
is triggered the browser displays an in-built confirmation alert message, but in case ofuseBlocker
we have our own confirmation modal. We useblocker.proceed()
method for continuing the navigation, in case it was intentional.
Conclusion: A safety net for users โจ
By implementing this unsaved changes alert system, we strike a balance between user experience and data protection. The user has the option to choose between completing the form or abandoning it, while being gently reminded.