Back and Forward Cache in React

1 year ago
|
0 views

TL;DR: If you develop a React component and there is a scenario where the user can navigate to an external URL and then go back to the previous screen, you should be aware of the back-forward cache in the browser. There are two common solutions to handle this issue:

  • Use ref to sync the state of the input element with the state of the component.
  • Use useEffect to reset the state of the component.

Issue When Navigating to an External URL and Going Back

Recently, when I implemented a feature in my work project, I encountered a strange issue in my React app.

For context, I have a screen that has a list of checkboxes and a Continue button. Each checkbox is associated with an external URL. When the user clicks on a checkbox, it navigates to the external URL, and the user can use the browser's back button to return to the previous screen. The Continue button is disabled by default and only becomes enabled when the user has checked all the checkboxes.

const PageAbc = ({ label, url }) => {
  const [checkedMapping, setCheckedMapping] = useState({});
 
  return (
    <div>
      {DATA.map((item) => (
        <div key={item.value}>
          <input
            type="checkbox"
            value={item.value}
            checked={checkedMapping[item.value]}
            onChange={() => {
              setCheckedMapping((prev) => ({
                ...prev,
                [item.value]: !prev[item.value],
              }));
            }}
          />
 
          <label htmlFor={item.value}>{item.label}</label>
 
          <a href={item.url} target="_blank" rel="noopener noreferrer">
            View detail
          </a>
        </div>
      ))}
 
      <button disabled={Object.values(checkedMapping).every(Boolean)}>
        Continue
      </button>
    </div>
  );
};

The issue is, if the user has already checked all the checkboxes, the Continue button becomes enabled. However, if the user then clicks an external URL and navigates away, upon clicking the back button the user still sees all the checkboxes as checked—but the Continue button remains disabled.

This leads to confusion and a bug, because the user must uncheck and then recheck the checkboxes to re-enable the Continue button.

After investigating, I discovered that the root cause was an inconsistency between the component state (checkedMapping) and the browser cache data. When the user navigates back to the previous screen, the component state resets to its initial values, but the browser cache still holds the previous state of the checkboxes (i.e., checked = true in the input elements).

You can reproduce this issue on this page.

So, what is the browser cache data called? It's called the back-forward cache, and it is enabled by default in most modern browsers.

The back-forward cache is a browser optimization that allows the entire page to be cached when the user navigates away, and then restores the page from the cache when the user navigates back, instead of re-rendering the page from scratch. It helps improve both performance and user experience, as the page is restored instantly without any loading time.

Since we manage the UI state in the React component, the back-forward cache will restore the page to its previous state; however, the restored UI does not synchronize with the component's state data.

Solution

Before looking for a solution, note that React will not trigger the unmount lifecycle when redirecting to an external URL! Therefore, any solution that relies on the unmount lifecycle will not work.

The simplest and yet effective solution is to reset the component's state when the user navigates back to the previous screen. We can do this by using useEffect with an empty dependency array.

useEffect(() => {
  // When the user navigates back to the previous screen, reset the state of the component
  setCheckedMapping({});
}, []);

However, this solution has some downsides:

  • Resetting the component's state in useEffect leads to an additional re-render after the initial render, which is not optimal for performance or user experience.
  • Depending on your business requirements, you may need to preserve the component's state when the user navigates back.

If you want to preserve the state of the component, another great solution is to use a ref to synchronize the state of the input element with the component's state.

const PageAbc = ({ label, url }) => {
  const [checkedMapping, setCheckedMapping] = useState({});
  const inputRefs = useRef([]);
 
  useEffect(() => {
    // This useEffect will run when the user navigates back
    // It will sync the input state cached by the browser with the state of the component
    const updateMapping = inputRefs.current.reduce((acc, ref) => {
      acc[ref.value] = ref.checked;
      return acc;
    }, {});
    setCheckedMapping(updateMapping);
  }, []);
 
  return (
    <div>
      {DATA.map((item, index) => (
        <div key={item.value}>
          <input
            type="checkbox"
            // Store the ref to sync with checkedMapping later
            ref={(ref) => (inputRefs.current[index] = ref)}
            value={item.value}
            checked={checkedMapping[item.value]}
            onChange={() => {
              setCheckedMapping((prev) => ({
                ...prev,
                [item.value]: !prev[item.value],
              }));
            }}
          />
 
          <label htmlFor={item.value}>{item.label}</label>
 
          <a href={item.url} target="_blank" rel="noopener noreferrer">
            View detail
          </a>
        </div>
      ))}
 
      <button disabled={Object.values(checkedMapping).every(Boolean)}>
        Continue
      </button>
    </div>
  );
};

In my opinion, this is the most flexible and reliable solution for resolving the BFCache issue in a React application. Moreover, it greatly improves the user experience, and users always appreciate a great UX.

It's also good practice to use a ref to synchronize the state of the input element with the component's state, as it helps avoid inconsistencies between the UI and the component's state.

This is a win-win solution for both developers and users.

Happy coding! 🚀