fix(auth): stop the login screen firing AccountSelect on every render #2364

Merged
requilence opened 2:42pm on September 4, 2026 wanted to merge 1 commit into anyproto/anytype-ts develop from
fix/JS-9871-double-account-select

Pull Request Overview

  • Opened on September 4, 2026
  • Status Merged
  • Commit count 1 with first commit September 4, 2026

Total Delta

0 Total Diff Delta

Open Days

Open 11 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Data pending calculation for pull request

Author avatar

fix(auth): stop the login screen firing AccountSelect on every render

Closes JS-9871

Problem

Logging in with a recovery phrase fires 4 AccountSelect RPCs on the same session token, and each one re-runs the entire post-login boot.

A gRPC capture of one login (800 request rows, one WalletCreateSession, one AccountRecover, one token, no extra tabs or windows) shows:


  • 4Γ— AccountSelect

  • 4Γ— each of NotificationList / FileNodeUsage / MembershipV2GetProducts / ChatSubscribeToMessagePreviews β€” i.e. U.Data.onAuthOnce() ran four times

  • 16Γ— ObjectCrossSpaceSearchSubscribe, 6Γ— WorkspaceOpen, 12Γ— ObjectOpen, 696Γ— ObjectSearchSubscribe

| t (ms) | event |
|---|---|
| 14086 | AccountSelect #1 (431 ms) |
| 14527 | onAuthOnce batch #1 |
| ~14586 | Animation.from callback re-opens the guard |
| 14652 | AccountSelect #2 (+66 ms) |
| 14805 | AccountSelect #3 |
| 16181 | AccountSelect #4 |
| 16982 | WorkspaceOpen β€” routing finally lands, page unmounts, loop ends |

Root cause

Accounts arrive as AccountShow events, not in AccountRecover's response β€” there is no Response.AccountRecover. So the login page watched the store instead: useEffect(() => { focus(); select(); }) with no dependency array, on a component auto-wrapped in observer by vite.auto-observer.ts. A render is being used as an event.

That left isSelecting as the only thing between a re-render and a fresh RPC β€” and on the success path it was cleared inside the Animation.from(...) callback, which fires after Animation.getDuration() (~50 ms), while routing only happens seconds later when onAuthWithoutSpace β†’ U.Subscription.createGlobal β†’ U.Space.openFirstSpaceOrVoid completes. Every render in that window fired another AccountSelect, and each success re-armed the guard, so the loop sustained itself until routing unmounted the page.

The error branch had the same hole: it re-opened the guard while S.Auth.accountList still held the account, and the two error codes that route away (FAILED_TO_FIND_ACCOUNT_INFO, ACCOUNT_STORE_NOT_MIGRATED) return from setErrorHandler before it reaches accountListClear().

Fix



  1. Keep the guard closed on success β€” the page is on its way out; nothing legitimate re-selects.


  2. Clear the account list wherever the guard re-opens, so the two always move together. Closes the identical loop on the error branch.


  3. Key the effect on the account list length instead of running select() on every render. This removes the amplifier β€” the actual root cause β€” and demotes the guard to a backstop. It also gives const length = accounts.length a real job; it previously existed only to register the MobX read.

History

Latent since 91993d1beb (2024-04-02, "fix possible double AccountSelect"), which added the guard and, in the same commit, the reset that re-arms it. 64a684cbdc (2025-04-17) added the if (spaceId) switchSpace(...) branch, which does not reset the guard β€” narrowing the bug to logins with no stored spaceId, i.e. first login on a device.

Follow-ups found while investigating (not in this PR)



  • U.Subscription.createGlobal runs twice per successful login round β€” tail of U.Data.onAuthOnce() and again from U.Data.onAuthWithoutSpace(); same at app.tsx. It opens with S.Record.spaceMap.clear() then destroyList β†’ Action.dbClearRoot, so the second run wipes the subId.space records the first is still filling. U.Space.getList() reads those and openFirstSpaceOrVoid routes to /main/void/error or /main/void/loading on zero β€” a candidate cause of spurious void screens on login.


  • src/ts/component/page/index.tsx has a block that computes nothing but reads account.status.type, subscribing PageIndex to every AccountUpdate and re-rendering every page child through its inline storageGet/storageSet closures. One confirmed re-render trigger in the capture above.


  • setup.tsx has no re-entrancy guard at all, only an empty dep array β€” one careless edit from the same bug.

Testing

typecheck and eslint clean. Needs a manual phrase login: the gRPC log should show exactly one AccountSelect.

https://claude.ai/code/session_016tzC8Yy6k8Dwdedic3U5R1

PR was closed without comments.