Preserving Form State in Refreshes and Navigation with React
Published: 2021-04-06 17:22:14

---
title: Preserving Form State in Refreshes and Navigation with React
published: true
date: 2020-04-07 00:00:00 UTC
tags: 
canonical_url: https://gal.hagever.com/posts/react-forms-and-history-state/
cover_image: https://gal.hagever.com/2eb5d33581d74419e317374370c12955/demo.gif
---

 **tl;dr:** You probably witnessed it as well. You fill a form, you navigate somewhere else and then go back to realize all the form was cleared away. Maybe it was a search form or even a registration one. This shouldn’t be the case: you can write a simple hook to preserve state in the browser history. [Try it live here](https://55hye.csb.app/). Also, [the code is live on CodeSandbox](https://codesandbox.io/s/usehistorystate-55hye).

* * *

These aren’t easy times. Ever since the COVID-19 pandemic stroke, I’ve been locked up to my apartment. Mainly working remotely and playing Overcooked 2 with my fiancée. Last week I had the chance to volunteer in a non-profit project, where I was writing some front-end code (forms!) in React. I haven’t written actual front-end code for a while, which was a fresh breeze.

The form itself was split to 3 pages, or “stages”, to make filling it easier:

![The form was split to 3 different stages](https://gal.hagever.com/static/782eb8e7577bf61064711857636e42d8/c0255/steps_excalidraw.png "The form was split to 3 different stages")

I started by coding every stage on its own. Adding navigation later should be easy and not a problem, thanks to [React Router](https://github.com/reacttraining/react-router) v5. This made my coding experience REALLY fast. When I got to navigation, using RRv5 was kinda easy. I decided to use the [browser history state](https://developer.mozilla.org/en-US/docs/Web/API/History/state) to pass the page results between pages. The browser history state is a way of passing data or storing data in the history stack itself, instead of adding a new global state to the app. So navigating looked like this:

```typescript
import { useHistory } from "react-router-dom";

function MyComponent() {
  const history = useHistory();

  return (
    <button
      type="button"
      onClick={() => {
        history.push({
          pathname: "/next-page",
          state: {
            firstSectionResult: THE_VALUE_I_WANT_TO_PASS
          }
        });
      }}
    >
      Next step
    </button>
  );
}
```

How does it work? Well, if you long-press the “back” button on your browser, you’d probably see your history for this specific tab. Using `window.history.back()` is a handy way to go back programmatically, but it’s not the entire API that the browser gives us. It also gives us the power to manipulate the history stack with pushing a new item and replacing the current item. Each item on the history has a path name and state that you can manage. Most project I witnessed don’t use the history state at all, despite it being a super powerful tool!

I also provided a hook to read the value off the history state:

```typescript
import { useHistory } from "react-router-dom";

export function useFirstSectionResult(): FirstSectionResult | undefined {
  const history = useHistory();
  const result = history.location.state?.firstSectionResult;
  return result;
}
```

Now I could use the history state in the next page just by calling this hook. If I got `undefined`, I could redirect the users back to the first section so they will complete it. I saw that this idea works, and I was excited to try pressing the browser’s “back” button to immediately press “next” to see that the state management actually works.

So I hit the “back button”. The form that I just filled is empty. When I clicked “next” the second stage of the form worked as expected, but I was very annoyed by the fact that all the fields I filled is now completely empty.

## Solving the State Preservation Problem[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#solving-the-state-preservation-problem)

This is a problem we have in modern front-end apps which we hadn’t when we were doing forms generated by some backend (like Rails or ASP) because the browsers do try to remember the values we filled in the forms! Because all the routing and rendering happens on the client side, way after the browser “rehydrates” its inputs, we lose all the state of the form.

This is clearly unacceptable because it’s the worst user experience there is. I discussed with the people who volunteered with me regarding the issue and consulted Google. Seems like like most solutions are using `localStorage`, `sessionStorage` and `indexedDB`. I decided to go with `sessionStorage` because both `localStorage` and `indexedDB` sounded like a long-time cache and a session-long cache sounds appealing to me.

I decided to go and make a custom hook (`useSessionState`) that worked like `useState`, only read the initial value from `sessionStorage`, and wrote all changes to `sessionStorage` as well. Then, I made all form elements controlled by specifying `onChange` and `value`.

Thinking about the solution again, I didn’t think it is good enough. The main problem I have with it, is that `sessionStorage` is _consistent_ across different browser tabs/windows. _It is basically a global cache._ That means that you can’t use the form in more than one tab(!). Not exactly what you expect from a web browser. Imagine you open multiple tabs and fill forms just to realize they override each other silently. Absurd!

This would also happen in `localStorage` and `indexedDB` because they too work like a global cache. So how can I still make the form work across different tabs while supporting refreshes and navigations?

### Altering the History State[<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#altering-the-history-state)

Remember the browser history state we have just used to provide state when navigating to a new page? We did it by calling the History `push` function. What if we could change the current page’s navigation state? Apparently, this is possible using the `replace` function, which _replaces_ the current item in the history stack instead of pushing a new one (which is the normal behavior of navigation). We can avoid altering the `pathname` (or URL) part, and only alter the state, like so:

```typescript
history.replace({
  ...history.location,
  state: {
    ...history.location.state,
    SOME_ITEM: SOME_VALUE
  }
});
```

This can be wrapped in a hook, to make it look and feel like the `useState` hook:

```typescript
import { useHistory } from "react-router-dom";

function useHistoryState<T>(key: string, initialValue: T): [T, (t: T) => void] {
  const history = useHistory();
  const [rawState, rawSetState] = useState<T>(() => {
    const value = (history.location.state as any)?.[key];
    return value ?? initialValue;
  });
  function setState(value: T) {
    history.replace({
      ...history.location,
      state: {
        ...history.location.state,
        [key]: value
      }
    });
    rawSetState(value);
  }
  return [rawState, setState];
}
```

Now, when we don’t have any plain `useState` in our form, every form input (and even complex JSON objects) will be preserved across refreshes and page navigations without further thinking. This solution isn’t just for people who use React Router. The attached CodeSandbox has a native implementation you can use anywhere. Make sure to open the preview in a new window because CodeSandbox’s preview pane does not preserve history state.

{% codesandbox usehistorystate-55hye %}

You see, making forms didn’t use to be hard. Making them work like they did while using modern front-end frameworks shouldn’t be either. Implementing this simple `setHistoryState` makes us stop thinking about how to manage forms’ state and lets us just write forms with a great experience. All thanks to the History API, which doesn’t only allow us to do magnificent client-side routing, it also helps us preserve meaningful state for our pages. 🎉

![A demo of using the browser history to preserve state](https://gal.hagever.com/2eb5d33581d74419e317374370c12955/demo.gif)