Skip to content

fix(provider): keep the session store alive across a remount - #119

Merged
Bccorb merged 1 commit into
mainfrom
fix/session-store-strictmode-remount
Jul 29, 2026
Merged

fix(provider): keep the session store alive across a remount#119
Bccorb merged 1 commit into
mainfrom
fix/session-store-strictmode-remount

Conversation

@Bccorb

@Bccorb Bccorb commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

AuthProvider creates the session store in useMemo but destroys it in a useEffect cleanup. Those have different lifetimes. React can run mount, cleanup, mount against the same memoized value: StrictMode does it on every mount, and Activity does it whenever a hidden tree is shown again.

destroy() is terminal by design, so the remounted provider held a store that refused every update. refreshSession then returned early at its destroyed guard before it could set loading: false, and loading stayed true forever.

Any app rendering the provider inside StrictMode, which is what templates/web/react-vite ships, never left its loading state. That applies to an authenticated session as much as an anonymous one, so it was not limited to signed-out first loads.

Found while running the react-vite and express templates against local main of every repo. The app sat on "Checking your session..." indefinitely.

Fix

The provider no longer destroys the store from its effect cleanup.

Nothing leaks by skipping it. useSyncExternalStore removes its own listener when the provider unmounts, and the store owns no timers or subscriptions, so it is reclaimed with the component. A refresh still in flight resolves into a store nobody observes.

destroy() stays on the store. Its terminal semantics are intentional and covered by authSession.test.ts, and a binding that genuinely owns the store's lifetime still needs it.

The store's contract was sound, so the fix is in the binding that misused it rather than in createAuthSession or in the adapter's response code.

Notes

The 400 that @seamless-auth/core returns for a missing seamless-access cookie is arguably better as a 401, since an absent cookie is the ordinary anonymous case. It is not what broke this, and changing it is a cross-repo contract change, so it is left alone here.

No public API change. The session store is not exported from src/index.ts, and dist/index.d.ts is unchanged.

The bug is unreleased: src/session does not exist in v0.6.0, so no published version is affected. Rather than add a changeset describing a bug no user ever saw, the pending nine-pans-shake.md changeset that introduced the store gained a paragraph on the remount behavior.

Verification

Both new tests fail on the previous behavior and pass on this branch.

  • npm run typecheck, npm run lint, npm run format:check: clean
  • npm test -- --runInBand: 271 passed, 31 suites, coverage thresholds met
  • npm run build, npm run check-npm-build: clean

Also exercised end to end against a local stack (auth API from source, express template, react-vite template) with StrictMode in place: registration, email OTP, and a signed-in session surviving a full page reload.

Activity is the same mechanism and the same fix, but only the StrictMode path was exercised.

AuthProvider created the session store in useMemo but destroyed it in an
effect cleanup. Those have different lifetimes: React can run mount,
cleanup, mount against the same memoized value, which StrictMode does on
every mount and Activity does whenever a hidden tree is shown again.
destroy() is terminal, so the remounted provider held a store that
refused every update, and refreshSession returned early before it could
clear loading. Any app rendering the provider inside StrictMode, which is
what the Vite template ships, stayed on loading forever, signed out or
not.

The provider no longer destroys the store. useSyncExternalStore removes
its own listener on unmount and the store owns no timers, so it is
reclaimed with the component. destroy() stays on the store for bindings
that genuinely own its lifetime.
@Bccorb
Bccorb merged commit 39ec6ac into main Jul 29, 2026
3 checks passed
@Bccorb
Bccorb deleted the fix/session-store-strictmode-remount branch July 29, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant