Representative interview topic

Frontend interview: How do you protect unsaved form data across page lifecycle changes?

FrontendHard
Offer.cc Editorial TeamPublished Updated

Question

A long form loses edits when a mobile browser freezes or discards its tab. How would you combine page lifecycle events, local drafts, server saves, and bfcache restoration?

Prompt and setting

You own a multi-step application form. Users switch tabs, lock a phone, navigate back, or leave the page for hours. The browser may freeze the page, restore it from bfcache, or discard it under memory pressure. The form must recover drafts without overwriting newer server data.

Assume the form contains sensitive but non-regulated data, the user can sign in on several devices, and network access is intermittent. The answer must state what is best effort and what is authoritative.

What the interviewer tests

  • Whether you distinguish visibility, freezing, page discard, and bfcache restoration.
  • Whether you avoid treating unload or beforeunload as a reliable persistence hook.
  • Whether local drafts have ownership, version, expiry, and conflict rules.
  • Whether recovery preserves user intent without silently replacing newer server data.

Clarifying questions before answering

  1. Is losing a local draft acceptable, or must the product provide a durable recovery guarantee? This determines whether local storage is a convenience or only a cache.
  2. Can the same draft be edited on several devices? If yes, the server needs a version or conflict policy.
  3. Is the data safe to store locally? Sensitive fields may require encryption, selective omission, or no local persistence.
  4. What is the submit contract? A draft save and final submit need different idempotency and validation rules.

30-second answer framework

“I treat the server as authoritative and the local draft as a bounded recovery cache. I persist a versioned draft on input changes with debounce, and flush a best-effort update when the document becomes hidden. I use pageshow to revalidate after bfcache restoration and resume to refresh stale state after a freeze. I never depend on unload. Each draft records an expiration, schema version, base server version, and dirty fields; conflicts are shown or merged explicitly rather than overwritten silently.”

Step-by-step deep dive

1. Model the lifecycle states

visibilitychange tells the page that it became hidden or visible; it does not promise that JavaScript will continue running. A browser may freeze a hidden page, and a discarded page may never run cleanup code. pagehide and pageshow describe navigation and bfcache restoration, while freeze and resume expose Chromium lifecycle transitions.

The design therefore saves before risk, not during a last-second unload. On visibilitychange to hidden, schedule a small local write and a best-effort network flush. On pagehide, stop nonessential work and record a local checkpoint. On pageshow, check event.persisted and revalidate the server version before displaying the restored form.

2. Make the local draft bounded and versioned

Store only fields that are safe to recover. A draft record can contain:

text
draft_id, user_id, form_schema, base_server_version,
changed_at, expires_at, dirty_fields, values

Use IndexedDB for structured data and a small local index for lookup. Debounce writes so typing does not create a write per keystroke, cap payload size, and delete expired drafts. A schema version lets the app migrate or discard an incompatible record instead of parsing it as current data.

The local record is not an authority. It is a user-device cache that may be missing, stale, duplicated, or deleted by the browser.

3. Flush safely when the page becomes hidden

When the document becomes hidden, first commit the local checkpoint, then attempt a small authenticated request if the product supports background saving. The request carries the draft ID and the server version it was based on. The server accepts it only if the version matches, then returns the new version.

Do not block navigation on a long request. If the request cannot finish, the local checkpoint still protects this device. Avoid a synchronous unload request: it harms navigation and is not guaranteed on mobile lifecycle paths.

4. Recover after bfcache and freeze

When pageshow fires with persisted, the page may contain a snapshot that is visually complete but logically old. Fetch the current server version, compare it with the form’s base version, and show a conflict choice if another device changed the draft. When resume fires, refresh session and data that may have expired while the page was frozen.

If the page was discarded, there is no in-memory state to resume. On the next load, look up an unexpired local draft, compare its base version with the server, and offer restore, discard, or merge. The user should see which values are newer before accepting a merge.

5. Test failure paths and privacy

Test tab switching, mobile app switching, back/forward navigation, bfcache restore, browser discard, offline edits, quota exhaustion, schema migration, expired drafts, and two-device conflicts. Assert that a newer server version is never silently overwritten.

Redact sensitive fields from logs. Clear drafts on account removal, honor a short retention period, and explain local recovery in the privacy notice. Measure restore success, conflict rate, local-write failures, and server-save latency; a “saved” indicator must represent a known checkpoint, not a hope that an unload handler ran.

High-quality sample answer

I would make the server versioned source of truth and keep a bounded local draft for recovery. Input changes are debounced into IndexedDB with a schema version, expiry, dirty-field set, and the server version used as the base. When visibilitychange reports hidden, I write the checkpoint and try a small authenticated save, but I do not block navigation or rely on unload.

On pageshow, especially when the event came from bfcache, I re-fetch the server version before trusting the restored form. On resume, I refresh data and session state that may have expired while frozen. A discarded page starts from a fresh load and offers the unexpired local draft. If versions differ, I show a conflict UI or a field-level merge; I never silently overwrite the newer server copy. Tests cover mobile discard, offline edits, quota limits, and two-device conflicts.

Common mistakes

  • Error: Saving only in beforeunloadWhy it fails: mobile browsers can terminate a page without firing it and it can block bfcache → Fix: checkpoint on input and hidden transitions.
  • Error: Treating a visible bfcache page as fresh → Why it fails: the snapshot can contain stale server state → Fix: revalidate on pageshow and compare versions.
  • Error: Making local storage the authority → Why it fails: it can be cleared, stale, or unavailable → Fix: use the server version and present local data as a recovery candidate.
  • Error: Retrying a whole form over a newer version → Why it fails: it overwrites unrelated edits → Fix: send dirty fields with an optimistic version and resolve conflicts.
  • Error: Logging draft values for debugging → Why it fails: sensitive content leaks into telemetry → Fix: log IDs, versions, sizes, and outcomes only.

Follow-up questions and responses

Should I use localStorage or IndexedDB?

Use localStorage only for tiny, synchronous metadata. IndexedDB fits structured drafts, larger payloads, and asynchronous writes. Neither gives durability or cross-device authority; both need expiry, quota handling, schema versioning, and privacy rules.

What if the user edits offline on two devices?

Give each save a base server version and device or draft ID. When connectivity returns, accept only a matching version, then show a field-level merge or let the user choose. A last-write-wins policy is acceptable only when the product explicitly accepts silent loss and the fields are independent.

Can a service worker guarantee the final save?

No. A service worker can improve retry delivery, but it can be stopped, lose network access, or be unavailable in a particular flow. The UI must report a confirmed checkpoint, and the server must make retries idempotent. The local draft remains the fallback for a device that cannot complete the request.

Public sources

Related questions