MetaMask / MetaMask/metamask-mobile
Memoize useWatchedNFTNames per-request flatMap/find scan and returned array
Nobody has claimed this yet.
- 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/useWatchedNFTNames.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/hooks/DisplayName/useWatchedNFTNames.ts:12`
### What is this about?
`useWatchedNFTNames` returns `requests.map(...)` with **no `useMemo`**, and for every request it re-derives `Object.keys(nftContractsByChainIdByAccount)`, builds a fresh `flatMap` of all NFT contracts for the chain across all accounts, then `.find()`s the match. This is O(requests × accounts × contracts) recomputed on every render, returning a fresh array each render.
```ts
return requests.map(({ type, value, variation }) => {
...
const accounts = Object.keys(nftContractsByChainIdByAccount); // per request, per render
const chainNfts = accounts.flatMap(
(account) => nftContractsByChainIdByAccount[account]?.[chainId] ?? [],
); // rebuilt per request, per render
const watchedNft = chainNfts.find(
(nft) => nft.address.toLowerCase() === contractAddress,
);
return watchedNft?.name ?? null;
});
```
**Why it matters**
This sub-hook is aggregated by `useDisplayNames`, which runs once per rendered name/address (confirmation screens, simulation rows, transaction lists, frequently per virtualized row). For wallets with many accounts and many watched NFTs, the per-request `flatMap` over all accounts' contracts is expensive and is repeated on every render. The fresh return array also defeats downstream memoization (compounding the wave-1 `useDisplayNames` issue).
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Evidence**
`app/components/hooks/DisplayName/useWatchedNFTNames.ts:12-30` — the whole body is an unmemoized `requests.map(...)`; `Object.keys(...)` (line 19) and the `flatMap` (line 21) execute inside the per-request callback on every render.
**Fix**
- Precompute a per-chain address→name index once with `useMemo` keyed on `[nftContractsByChainIdByAccount]` (flatten all accounts' contracts into `Map>`), then do O(1) lookups per request.
- Wrap the final `requests.map(...)` in `useMemo` keyed on `[requests, index]` so the returned array is stable.
### 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/useWatchedNFTNames.test.ts`.
- Add a test rendering twice with identical `requests` and unchanged Redux state; assert returned array is `===` across renders.
### References
- File: `app/components/hooks/DisplayName/useWatchedNFTNames.ts:12`
- Source: MetaMask Mobile performance audit — finding `unstablehook-usewatchednftnames-on2-flatmap-every-render`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- Status: **UNVALIDATED**
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with app/components/hooks/DisplayName/useWatchedNFTNames.ts and read the existing coverage in app/components/hooks/DisplayName/useWatchedNFTNames.test.ts. Run yarn jest app/components/hooks/DisplayName/useWatchedNFTNames.test.ts first, then verify the per-chain lookup work and returned request results are memoized without changing behavior. Done means the added repeated-render identity test passes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100