Back and forward cache in react

4 months ago
|
0 views

TLDR: if you develop an react component and their is an senerio that user can navigate to external URL and back to the previous screen, you should be aware of the back-forward cache in the browser. Their are 2 common solution to handle this issue:

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

Issue when nagivate to external URL and go back

Recently, when I implement a feature in my project at work, I experienced a weird issue in my react app.

For some context, I have an screen that have an list of checkboxes and an continue button, each checkbox will come with an external URL, when user click on the checkbox, it will navigate to the external URL, and user can use browser back button to go back to the previous screen. Continue button will disabled by default, and only enable when user 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 user already checked all of the checkboxes, the continue button will enable, but then user click on external URL and navigate to it, then click back button, user still see all of the checkboxes are checked, but the continue button is disabled.

This lead to some confusion for the user, and bug also, because user will need to uncheck and check again to enable the continue button again.

After some investigation, I found out that the root cause is because of the inconsistency between component state checkedMapping and the browser cache data. When user navigate back to the previous screen, the component state is empty as initial state, but the browser cache still have the previous state of the checkboxes (checked = true in input element).

You can try this issue in this page.

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

The back-forward cache is a browser optimization that allows to cache the whole page when user navigate away from it, and when user navigate back to the page, the page will be restored from the cache, instead of re-rendering the page from scratch. It help to improve the performance and user experience, because the page will be restored instantly, without any loading time.

Since we manage the UI state in the react component, and the back-forward cache will restore the page to the previous state, but the restored UI won't sync with your state data

Solution

Before we looking for solution, we need to take note that react will not trigger unmount lifecyle when redirect to external URL !!, so any solution involve with unmount lifecyle will not work.

The most simple and yet effective solution is reset the state of the component when user navigate back to the previous screen. We can use useEffect with [] as dependency array.

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

But this solution have some downside:

  • Reset the state of the component in useEffect lead to re-render after initial render, and it's not good for performance or user experience.
  • Depend on business requirement, you may need to keep the state of the component when user navigate back

So if you want to keep the state of component, another great solution is use ref to sync the state of input element with the state of the component.

const PageAbc = ({ label, url }) => {
  const [checkedMapping, setCheckedMapping] = useState({});
  const inputRefs = useRef([]);
 
  useEffect(() => {
    // This useEffect will run when user navigate back
    // it will sync the input state that cached by 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 ref value 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>
  );
};

IMO, this is the most flexible and reliable solution when it come to solve BFCache issue in React application. Moreover, it improve user experience a lot and user always love great UX app.

It's also a good practice to use ref to sync the state of input element with the state of the component, because it's help to avoid the inconsistency between the UI and the state of the component.

So, a win-win solution for both developer and user.

Happy coding! 🚀