MetaMask / MetaMask/metamask-mobile
Memoize useAccountNames account-group map build and returned array
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
> **Performance audit finding** · Severity: **Medium** · Effort: Easy · Fix risk: Simple · Test safety net: Covered (app/components/hooks/DisplayName/useAccountNames.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/hooks/DisplayName/useAccountNames.ts:11`
### What is this about?
`useAccountNames` rebuilds a full `accountGroupNames` lookup map on **every render** by iterating every account group and every account inside it, with no `useMemo`. It then returns a fresh `.map()` array (line 22) — also unmemoized.
```ts
const accountGroupNames = accountGroups.reduce((acc, group) => {
group.accounts.forEach((accountId) => {
const account = internalAccountsById[accountId];
acc[account.address.toLowerCase()] = group.metadata.name;
});
return acc;
}, {} as Record);
return requests.map((request) => {
const { value } = request;
return accountGroupNames[value.toLowerCase()];
});
```
**Why it matters**
This hook is one of the inputs aggregated by `useDisplayNames` (the wave-1 `unstablehook-usedisplaynames-fresh-array` finding fixes only the top-level array). `useDisplayName`/`useDisplayNames` runs once per name/address rendered — in confirmation screens, simulation rows, and transaction lists, frequently per-row in virtualized lists. For a wallet with many accounts/groups, the `reduce`-over-all-groups runs on every render of every row, and the returned array is a new reference each time, defeating downstream memoization in `useDisplayNames`.
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Evidence**
`app/components/hooks/DisplayName/useAccountNames.ts:11` (map rebuild) and `:22` (fresh return array). Both run unconditionally on every render because nothing is wrapped in `useMemo`.
**Fix**
Wrap the map build in `useMemo` keyed on `[accountGroups, internalAccountsById]`, and wrap the final `requests.map(...)` in `useMemo` keyed on `[requests, accountGroupNames]`:
```ts
const accountGroupNames = useMemo(() => accountGroups.reduce(...), [accountGroups, internalAccountsById]);
return useMemo(() => requests.map((r) => accountGroupNames[r.value.toLowerCase()]), [requests, accountGroupNames]);
```
(Pair with the wave-1 fix that stabilizes the `requests` array passed in from `useDisplayName`.)
### Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
### Acceptance Criteria
- - Run `yarn jest app/components/hooks/DisplayName/useAccountNames.test.ts`.
- Add a test rendering the hook twice with identical `requests` and unchanged Redux state; assert the returned array is `===` across renders.
### References
- File: `app/components/hooks/DisplayName/useAccountNames.ts:11`
- Source: MetaMask Mobile performance audit — finding `unstablehook-useaccountnames-rebuild-map-every-render`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- Status: **UNVALIDATED**
Contributor guide
Research direction
Start with app/components/hooks/DisplayName/useAccountNames.ts, then run app/components/hooks/DisplayName/useAccountNames.test.ts. Review the account-group lookup and returned request-name array, and add coverage for repeated renders with unchanged inputs; done means the existing test suite passes and the returned array remains referentially stable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, mobile
- Issue type
- Refactor
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100