A multi-step form that loses progress when the user refreshes or closes the tab is worse than a form that never promised to save anything. Users who partially complete a lengthy application -- an insurance quote, an account setup, a loan request -- and return to an empty form often do not start over. They leave and sometimes do not come back. Persisting form state to the browser's storage APIs is a low-complexity change that has a direct impact on completion rates. This guide covers how to implement it correctly, which storage mechanism to choose, when to expire saved state, and what must never be stored.

Photo by freestocks.org on Pexels
localStorage vs. sessionStorage: The Core Difference
Both APIs are part of the Web Storage API and share nearly identical interfaces. The key difference is lifetime.
localStorage persists until you explicitly clear it or the user clears their browser data. Data survives tab closures, browser restarts, and multi-day gaps. If you save a form state to localStorage at 2 PM today, it will still be there at 10 AM next Tuesday unless you explicitly remove it or the user clears their storage. Use localStorage for forms where users may return after an extended break -- insurance applications, B2B onboarding flows, loan applications, any form that realistically takes more than one sitting to complete.
sessionStorage persists only for the duration of the browser session. It is cleared when the tab is closed. It is not shared between tabs -- two tabs on the same origin have separate, independent sessionStorage. Use sessionStorage for forms where partial data should not persist across sessions: high-security contexts, checkout flows where cart contents expire, or forms with short-lived validity windows.
For the majority of multi-step forms, localStorage is the right default. The risk of someone returning to a pre-filled form and being confused by stale data is lower than the risk of losing a user who was interrupted mid-flow.
The Core Persistence Pattern
The implementation has two halves: write state to storage on every significant change, and read from storage on mount to restore a previous session.
Writing on change is straightforward in any framework with a reactive state model. In a React application, the pattern is to use a side effect that fires whenever the form state changes. You serialize the values object, the current step indicator, and a timestamp to JSON, then write to localStorage with a consistent key. The timestamp is critical -- it lets you implement expiry on the read side.
Reading on mount involves checking whether a saved entry exists, whether the timestamp is within your expiry window, and if so, initializing the form state from the saved values rather than the empty defaults. If the entry does not exist or is expired, initialize from defaults and remove the stale key.
In React, the natural pattern is a useState initializer function that checks for saved state synchronously before the component renders. This avoids a flash of empty fields followed by a re-render with restored values. The initializer runs once, before the first render, and the form starts in the correct state.
Integrating With Form Libraries
Most React form management libraries work cleanly with this persistence pattern. The form library manages field registration, validation, and error state. The persistence layer is a separate concern that reads and writes the values object to localStorage.
With React Hook Form, use watch() to subscribe to all field values and trigger a persistence effect whenever they change. Use reset() on mount to initialize the form with restored values. The library's getValues() method is available for reading the current complete state before navigating between steps.
With Formik, the values property in the render prop or hook provides the current state, which you pass to your persistence function in a useEffect. On initialization, use initialValues set to the restored state or the empty defaults depending on whether a saved session exists.
One practical note: form libraries that use uncontrolled inputs (React Hook Form's default mode) synchronize internal ref state to the form values on registration. When you restore values on mount, call reset() with the restored values after the form initializes rather than before, to ensure the refs are populated correctly.
Handling Expiry and Cleanup
Saved state that is never cleaned up accumulates in the user's localStorage and can cause confusing restoration of very old form sessions. Implement at minimum three cleanup paths:
First, check age on load. Compare the stored timestamp to the current time. If the difference exceeds your expiry window (24 hours for a checkout flow, 72 hours for a lead capture form, perhaps 7 days for a complex onboarding wizard), delete the stored entry and start fresh. Show a brief notice if the form is prominent: "Your previous session has expired. Starting a new one."
Second, clear on successful submission. When the user completes the final step and the server returns a success response, remove the localStorage entry. A returning user should not see a form pre-filled with data from a completed session.
Third, provide an explicit reset action. For forms with sensitive context, offer a "Start over" button that clears the stored state and resets the form to empty. Some users completing a shared device flow or returning after a significant context change will want this.
What Not to Persist
The following field types should never be written to localStorage:
Payment card numbers and CVVs are the obvious case. These must not be stored in browser storage under any circumstances. Payment fields should always require re-entry and should be handled by a PCI-compliant payment library that isolates them from your application's state.
Passwords and authentication tokens also must not be persisted. If your form has a step that includes a password field, omit that field from the serialized state entirely. When the user returns, they fill the password field again -- it is a small inconvenience compared to the security risk.
One-time codes -- verification tokens sent by email or SMS -- expire server-side and should not be saved. When the user returns to a form that included a verification step, they should trigger a new code send.
Computed or derived values should not be persisted unless the underlying inputs are also persisted. A price calculated from user inputs may be stale if pricing changed between the user's first visit and their return. Store the raw inputs, recalculate on restore, and present the recalculated value.
Prompting Users About Saved State
Silently pre-filling a form from localStorage can be disorienting, especially for users who do not remember starting the form. A brief, non-intrusive banner or dialog at the top of the form -- "We found a saved draft from 2 days ago. Resume or start over?" -- gives users agency and makes the persistence feature visible and trustworthy rather than mysterious.
This prompt requires storing additional metadata alongside the form values: the step the user was on, the date, and optionally a summary of what was filled. The user's response determines whether you initialize from saved state or clear it and start fresh.
Testing Persistence
The persistence layer should be tested independently of the form library. Write unit tests that confirm: state is written to localStorage after changes, expired state is not loaded, cleared state produces empty initialization, and submission triggers cleanup. For integration testing, use Playwright or Cypress to simulate a page reload mid-form and assert that the fields are still populated with the values entered before the reload. A test that fills step one, reloads the page, and asserts that step one values are present is one of the more valuable regression tests for any multi-step form.
For a complete guide on building the full multi-step form flow -- including state management architecture, per-step validation, back navigation, and accessibility -- see How to Build a Multi-Step Form Flow That Saves User Progress.

Photo by Luis Quintero on Pexels
137Foundry's web development team builds multi-step form flows for onboarding, lead capture, and application processes that need high completion rates across device types.
Top comments (0)