Skip to content

fix(auth): backfill login form when injected defaults arrive after mount - #33

Open
ggfto wants to merge 1 commit into
WW-AI-Lab:mainfrom
ggfto:fix/login-form-prefill-race
Open

fix(auth): backfill login form when injected defaults arrive after mount#33
ggfto wants to merge 1 commit into
WW-AI-Lab:mainfrom
ggfto:fix/login-form-prefill-race

Conversation

@ggfto

@ggfto ggfto commented Aug 24, 2026

Copy link
Copy Markdown

Problem

A deployment can inject gatewayUrl and gatewayToken into the page, and the
sign-in form still renders empty. Reloading never helps.

LoginGate seeds its local state from the auth store:

const gatewayUrlInit = useAuthStore((s) => s.gatewayUrl);
const [gatewayUrl, setGatewayUrl] = useState(gatewayUrlInit);

But that store is populated by an effect in <App>:

useEffect(() => {
  hydrate({ gatewayUrl, token: gatewayToken });
}, [hydrate, gatewayUrl, gatewayToken]);

React runs a parent's effects only after its children have mounted, so
LoginGate always reads the pre-hydration store — two empty strings. A plain
re-render never revisits a useState initializer, so the fields stay blank for
the lifetime of the page. The mount order is deterministic, which is why
reloading has no effect.

Fix

Backfill both fields once the defaults arrive, using functional updates so that
anything the user has already typed always wins:

useEffect(() => {
  setGatewayUrl((current) => current || gatewayUrlInit);
}, [gatewayUrlInit]);

18 lines in one component, plus tests.

What this deliberately does not change

The credential-restore policy stays exactly as it is. loadStoredCredentials()
is still never called on startup, which auth-store.ts explains is intentional:
a stale token or URL from a previous deployment would start a silent connect
attempt, leave the UI on "connecting", and block the user from submitting a
corrected value. This PR only fixes the prefill path, which is the one that is
already meant to run.

Tests

src/components/auth/LoginGate.test.tsx, two cases:

  1. Backfills when defaults arrive after mount. Renders first, hydrates
    second — the same order the browser produces. This case fails without the
    change: expected '' to be 'ws://gateway.test/gateway-ws'.
  2. Keeps values the user already typed, so a late default cannot clobber a
    correction in progress.

Full suite: 64 files, 546 tests passing. tsc --noEmit clean.

I left the repo's existing formatting untouched — running oxfmt with default
settings wanted to reflow unrelated lines in the same file, and that noise did
not belong in a fix this small.

`LoginGate` seeds its local state with `useState(gatewayUrlInit)` /
`useState(tokenInit)`, but the store those values come from is populated by an
effect in `<App>`:

    useEffect(() => {
      hydrate({ gatewayUrl, token: gatewayToken });
    }, [hydrate, gatewayUrl, gatewayToken]);

React runs a parent's effects only after its children have mounted, so
`LoginGate` always reads the pre-hydration store — two empty strings. A plain
re-render never revisits a `useState` initializer, so the fields stay blank for
the lifetime of the page.

The result is a deployment that injects `gatewayUrl` and `gatewayToken` into the
page, and still shows the user an empty sign-in form. Reloading does not help,
because the mount order is the same every time.

Fix: backfill both fields once the defaults arrive, using functional updates so
that anything the user has already typed always wins.

This does not change the credential-restore policy. `loadStoredCredentials()` is
still never called on startup, as `auth-store.ts` documents on purpose: a stale
token from a previous deployment would start a silent connect attempt and block
the user from submitting a corrected value.

Tests: the first case fails without this change
(`expected '' to be 'ws://gateway.test/gateway-ws'`); the second guards against
a late default clobbering user input.
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