Back and Forward Cache in React
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! 🚀