React Form Wizard
Guides

Persistence & URL Sync

Keep wizard data across reloads and mirror the active step into the URL.

Persistence & URL Sync

v1.2

Long forms lose people when a refresh wipes their answers. persist keeps the data; syncToUrl keeps the position.

Data goes to sessionStorage["docs-persist"], and the active step is mirrored into ?docs-step=. Reload this page to see both restored.

Form wizard with 3 steps. Currently on step 1.

Resumable wizard

Step 1 of 3: Write
<FormWizard
  // Survives a reload. "session" clears with the tab; "local" persists.
  persist={{ key: "checkout", storage: "session" }}
  // Mirrors the step into ?step=2 so refreshes and shared links land correctly.
  syncToUrl
  schema={schema}
/>

persist

OptionTypeDefaultDescription
keystringStorage key. Namespace it per form.
storage"session" | "local""session"session clears when the tab closes; local survives it.

Stored data is merged over initialData on mount, so new fields you add later still get their defaults.

Clear it with ref.current.reset() (which also returns to the start step) or, in the headless API, wizard.clearPersisted().

const wizardRef = useRef<FormWizardMethods>(null);

<FormWizard
  ref={wizardRef}
  persist={{ key: "checkout" }}
  onComplete={async (data) => {
    await submit(data);
    wizardRef.current?.reset(); // clears the saved payload too
  }}
/>;

URL sync

syncToUrl writes the active step into a query parameter, 1-based so the URL reads naturally (?step=2 is the second step).

<FormWizard syncToUrl />                      // ?step=2
<FormWizard syncToUrl={{ param: "stage" }} /> // ?stage=2

It uses history.replaceState, so it does not add browser-history entries — Back leaves the wizard rather than stepping through it. On mount, a step present in the URL wins over startIndex.

Safety

Both features are best-effort by design:

  • Guarded for SSR — window is never touched during a server render.
  • Private browsing, disabled storage and quota errors are swallowed, not thrown. A failed write never breaks the form.
  • Malformed stored JSON is ignored and the wizard falls back to initialData.

sessionStorage and localStorage are readable by any script on the origin and are not encrypted. Keep card numbers, passwords and tokens out of wizard data when persistence is on — persist an id and re-fetch instead.

With the headless API

Identical options:

const wizard = useWizard({
  stepIds: ["account", "profile", "review"],
  persist: { key: "signup", storage: "local" },
  syncToUrl: { param: "s" },
});

See also: Props, Headless.

On this page