MetaMask / MetaMask/metamask-mobile
Memoize useERC20Tokens assetIds input 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/useERC20Tokens.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/hooks/DisplayName/useERC20Tokens.ts:8`
### What is this about?
`useERC20Tokens` builds a fresh `assetIds` array (`.filter().map()`) on every render and passes it to `useTokensData`, then returns a fresh `requests.map(...)` array — neither is memoized.
```ts
const assetIds = requests
.filter(({ type, value }) => type === NameType.EthereumAddress && value)
.map(({ value, variation }) =>
buildEvmCaip19AssetId(value as string, variation as Hex),
); // fresh array every render
const tokensByAssetId = useTokensData(assetIds); // re-derives from a new array each render
return requests.map(({ preferContractSymbol, type, value, variation }) => {
...
return { name, image: token?.iconUrl }; // fresh objects + fresh array every render
});
```
**Why it matters**
This sub-hook is aggregated by `useDisplayNames`, which runs per rendered name/address (confirmations, simulation rows, transaction lists, per virtualized row). The returned array — and the inner `{ name, image }` objects — are new references on every render, defeating downstream memoization the wave-1 `useDisplayNames` finding aims to restore. `useTokensData` internally keys its effect on the joined string (`assetIds.join(',')`) so the network side is safe, but the recreated `assetIds` array and the fresh return array/objects still churn references on every render and re-run `buildEvmCaip19AssetId` for every request twice per render (once for `assetIds`, once in the return map).
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Evidence**
`app/components/hooks/DisplayName/useERC20Tokens.ts:8-12` (fresh `assetIds`), `:16-27` (fresh return array of fresh objects). No `useMemo` anywhere in the hook.
**Fix**
- Memoize `assetIds` with `useMemo` keyed on `[requests]`.
- Memoize the final `requests.map(...)` with `useMemo` keyed on `[requests, tokensByAssetId]` so the returned array and its objects are stable when inputs are unchanged.
### 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/useERC20Tokens.test.ts`.
- Add a test rendering twice with identical `requests` and the same `useTokensData` result; assert the returned array reference is `===` across renders.
### References
- File: `app/components/hooks/DisplayName/useERC20Tokens.ts:8`
- Source: MetaMask Mobile performance audit — finding `unstablehook-useerc20tokens-fresh-assetids-and-array`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- Status: **UNVALIDATED**
Contributor guide
Research direction
Start with app/components/hooks/DisplayName/useERC20Tokens.ts and inspect the existing coverage in app/components/hooks/DisplayName/useERC20Tokens.test.ts. Memoize the assetIds calculation and returned mapping as described, then run yarn jest app/components/hooks/DisplayName/useERC20Tokens.test.ts and verify that identical requests and useTokensData results preserve the returned array reference across renders.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- performance, testing-qa
- Issue type
- Refactor
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100