MetaMask / MetaMask/metamask-mobile
Memoize useDisplayNames return array to stop downstream re-renders
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
> **Performance audit finding** · Severity: **High** · Effort: Easy · Fix risk: Simple · Test safety net: Covered (app/components/hooks/DisplayName/useDisplayName.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/hooks/DisplayName/useDisplayName.ts:154`
### What is this about?
`useDisplayNames` builds and returns a brand-new array of brand-new objects on every render via `requests.map(...)` with no `useMemo`. Every consumer therefore receives a referentially-new array/objects each render, defeating any `React.memo`, `useMemo`, or dependency-array memoization downstream that keys off the returned display-name objects.
**Why it matters**
`useDisplayName`/`useDisplayNames` is used broadly in confirmation screens, simulation rows, name components, and transaction lists — frequently inside lists rendered per-row. Returning fresh references forces children that take a display-name object as a prop to re-render on every parent render even when nothing changed. In SimulationDetails specifically, the result is fed into `useSimulationMetrics` and into row components, so churn here amplifies.
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Evidence**
`app/components/hooks/DisplayName/useDisplayName.ts:154`
```ts
return requests.map(({ value, variation }, index) => {
...
return {
contractDisplayName: erc20Token?.name,
image,
isFirstPartyContractName,
name,
subtitle,
variant: getVariant({ name, accountName }),
displayState,
icon,
isAccount: Boolean(accountName),
};
});
```
The single-item helper compounds it:
`app/components/hooks/DisplayName/useDisplayName.ts:135`
```ts
export function useDisplayName(request: UseDisplayNameRequest): UseDisplayNameResponse {
return useDisplayNames([request])[0];
}
```
`[request]` is itself a fresh array literal each render, so even the sub-hooks (`useFirstPartyContractNames`, `useERC20Tokens`, etc.) receive a new `requests` reference each call.
**Fix**
Wrap the final `requests.map(...)` in a `useMemo` keyed on the derived inputs (`requests`, `firstPartyContractNames`, `watchedNftNames`, `erc20Tokens`, `accountNames`, `accountWalletNames`, `trustSignals`, and the ENS resolver). For `useDisplayName`, memoize the single-element request array (`useMemo(() => [request], [request.type, request.value, request.variation, request.preferContractSymbol])`) before passing it to `useDisplayNames`, so the sub-hooks also see stable inputs.
### 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/useDisplayName.test.ts` to confirm behavior is unchanged.
- Add/extend a render-count test: render a consumer twice with identical `requests` and assert the returned reference is `===` across renders (currently it is not).
### References
- File: `app/components/hooks/DisplayName/useDisplayName.ts:154`
- Source: MetaMask Mobile performance audit — finding `unstablehook-usedisplaynames-fresh-array`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- Status: **UNVALIDATED**
Contributor guide
Research direction
Start with app/components/hooks/DisplayName/useDisplayName.ts at the useDisplayNames mapping and the single-item useDisplayName helper. Run app/components/hooks/DisplayName/useDisplayName.test.ts, then add or extend the render-count coverage described in the issue. Done means identical inputs preserve the returned reference while existing behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- frontend, mobile, performance, testing-qa
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100