Build1 distinct publisher3 min readUpdated
Persisting wizard state in localStorage ships a cross-tab bleed. sessionStorage is scoped to one tab, which is the lifetime a payment redirect actually needs.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A multi-step checkout holds its state in `useState`, the payment provider bounces the customer out to a 3-D Secure page and back, and the wizard returns on step one with an empty form, because `useState` lives exactly as long as the component instance and a reload or full-page navigation ends it [1][2]. The reflexive fix is `localStorage`, and it is the wrong default: according to a dev.to write-up on the `useSessionStorage` hook, the half-finished checkout then appears in every tab the customer has open, is still sitting there next week, and if they open two tabs to compare shipping options a `localStorage`-backed hook syncs the two forms into each other [3].
What `sessionStorage` scopes to is one top-level browsing context, meaning a tab or window, for one origin. It does not mean a logged-in session and it does not mean a browser session, which is where the name misleads people [4]. That lifetime is the one a wizard wants: state survives this tab's reloads, a redirect out to a payment provider and back, and the browser's back button, then disappears when the tab closes [5].
The heuristic in the post is worth stealing verbatim. If two tabs showing different values would be a bug, use `localStorage`; if two tabs showing the same value would be a bug, use `sessionStorage` [6]. Theme, language and a "don't show this again" dismissal are expected to be one value everywhere, so local [7]. A half-completed form, the filters on this dashboard view, and the page you were on before an auth redirect belong to one tab, so session [8].
Per-tab is not airtight, and the exceptions are worth knowing before you rely on the isolation. `window.open()` copies the opener's `sessionStorage` into the new window whenever that window keeps an `opener`, per the HTML spec, and Chrome's Duplicate tab copies it too; it is a one-time snapshot rather than a live link, so the two diverge from then on [9]. Modern browsers open `target="_blank"` links with `noopener` by default, so ordinary links start clean [10]. Same-origin iframes in the same tab do share it, since they sit in the same browsing context group, and that is the only place the browser's native `storage` event means anything for `sessionStorage` [11]. Two documented holes, both of them copy-on-open or same-tab, neither of them the live cross-tab sync that `localStorage` gives you for free [12].
Everything else is the same contract as `localStorage`: synchronous, string-only, roughly 5 MB per origin, and readable by any script on the page, so it is not a security boundary [13]. The shorter life limits the blast radius of a leak, but XSS reads it just as easily, and anything that must be secret from JavaScript belongs in an `httpOnly` cookie [14].
The article is, in fairness, a pitch for `useSessionStorage` from `@reactuses/core`, which returns the same `[value, setValue]` tuple as `useState`, reads on mount, writes on every update, and types the value `T | null` because `setValue(null)` removes the key [15]. Its more useful admission is that the hook shares its API, serialization and internals with `useLocalStorage`, so swapping the import changes the lifetime and nothing else [16]. The migration cost of getting this right is one line, which removes the usual excuse for not deciding.
Worth checking: your existing `localStorage` keys, for anything that is per-flow rather than per-user. And if you hand-roll the hook rather than take the dependency, the post flags hydration, tab restore and `window.open` as the cases that bite [17].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
useSessionStorage from @reactuses/core, installed via npm install @reactuses/core, returns the same [value, setValue] tuple as useState with the same functional updates; the value is read from sessionStorage on mount, written back on every update, and typed T | null because setValue(null) removes the key.
In a checkout flow with steps for address, shipping and payment, the payment provider redirects the customer out to a 3-D Secure page and back, or the customer hits refresh, and the step counter is 0 again with an empty form.
useState lives exactly as long as the component instance does; a reload, a redirect or a full-page navigation destroys it.
Most people reach for localStorage, and it works until it works too well: the half-finished checkout sits in every tab the customer opens, is still there next week, and if they open two tabs to compare shipping options useLocalStorage syncs the two forms into each other.
The name sessionStorage misleads people into thinking session means logged-in session or browser session; it means one top-level browsing context, a tab or window, for one origin.
With sessionStorage-backed state, reloading the page, being redirected to a payment provider and back, or navigating away and using the browser's back button leaves step and form exactly where the customer left them; closing the tab removes them.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Single first-party tutorial; spec-checkable mechanics, unverified library claims
The mechanism claims are specific, internally consistent and anchored to well-known Web Storage/HTML behaviour (per-tab scope, window.open copy-on-open snapshot, noopener default, same-origin iframe sharing, ~5 MB per origin, no security boundary), which raises evidence quality above pure assertion. But the cluster contains exactly one source, which is also the promoter of the recommended package, and it supplies no tests, benchmarks, repository links or third-party confirmation for the hook's hydration, serialization and error-fallback behaviour.
No adoption data in cluster
The supplied material contains only an npm install instruction and code examples. There are no download counts, production deployments, release notes, version history, issue activity or user disclosures for @reactuses/core, and no data on how widely sessionStorage-backed wizard state is used in practice. Nothing supports a measured adoption score.
Modest, mechanism-level claims with one promotional overreach
The technical claims are deliberately narrow and mostly restate documented browser behaviour, so the gap is small rather than large. It is positive because the article converts a genuine but bounded bug class into a package recommendation: it asserts the hooks share 'the exact same API, serialization and internals' and that hand-rolled versions get hydration, tab restore and window.open wrong, without demonstrating either, and offers no adoption or maintenance evidence for the dependency it asks readers to install.
Tutorial whose fix is one named package, plus cross-promotion
The article's recommended remedy is a specific npm package, @reactuses/core, complete with install command, import and an argument that hand-rolled equivalents are defective; it also directs readers to the author's companion useLocalStorage deep-dive. That is a clear promotional incentive shaping selection and framing. The supplied material does not state the author's relationship to the package, so the score reflects observable promotion and self-referencing rather than an established maintainer interest, and is tempered by the article volunteering security and scope limitations.
Confident about the bug class, thin on the recommended remedy
Confidence is limited by cluster structure: one publisher, one author, no adoption measurement. The problem statement and browser-scope semantics are stated clearly enough and align with widely documented Web Storage behaviour, so those parts are reliable. The claims about the library's internals, SSR handling and superiority over hand-rolled hooks are single-source and promotional, and the article body is truncated mid-list, leaving part of its argument unread.
build
The Escape-key listener you cannot fix with a dependency array1 distinct publisher
build
The React counter that sticks at 1, and why no dependency array will fix it1 distinct publisher
build
The stale closure is not React trivia, it is a correctness bug with a five-line fix1 distinct publisher
build
Codex learns to click: the coding agent stops typing patches and starts operating the machine1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 16, 2026