MetaMask / MetaMask/metamask-mobile

Memoize useDisplayNames return array to stop downstream re-renders

Open
#31,375 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev3 size-S ta-ai-fixable ta-triaged team-confirmations
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.