Preventing Data Loss: Implementing Unsaved Changes Alert in React App ๐ŸŽฏ

Preventing Data Loss: Implementing Unsaved Changes Alert in React App ๐ŸŽฏ

ยท

5 min read

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 ๐Ÿ”–

  1. beforeunload Event: Inside the useEffect hook, we register the beforeunload 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.

  2. 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)

  3. Whenever beforeunload is triggered the browser displays an in-built confirmation alert message, but in case of useBlocker we have our own confirmation modal. We use blocker.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.

References ๐Ÿงพ

ย