MetaMask / MetaMask/metamask-mobile
Memoize SendContext provider value to stop re-rendering all 26 consumers
- 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: Partial
> Owner (CODEOWNERS): `@MetaMask/confirmations`
> File: `app/components/Views/confirmations/context/send-context/send-context.tsx:108`
### What is this about?
`SendContextProvider` passes a brand-new inline object literal to `SendContext.Provider value={{ ... }}` on every render. Because the object identity changes each render, every component that calls `useSendContext()` re-renders whenever the provider re-renders, even if the specific field it reads did not change.
`useSendContext` has **26 consumer files** across the Send flow (amount input, recipient input, asset pickers, review screens, etc.). The provider holds several `useState` values (`asset`, `to`, `value`, `maxValueMode`, `fromAccount`) plus derived `chainId`. Typing in the amount field calls `setValue` on every keystroke, re-rendering the provider and therefore the entire Send subtree — not just the amount component. On the Send flow this is a per-keystroke fan-out.
### Scenario
```gherkin
GIVEN a user is on the Send flow
WHEN they type a digit in the amount field (setValue fires on every keystroke)
THEN the SendContext provider re-renders
AND all 26 useSendContext() consumers re-render — not only the amount component
```
### Design
N/A — internal performance refactor; no UI or design change.
### Technical Details
Current code — `app/components/Views/confirmations/context/send-context/send-context.tsx:107`:
```tsx
return (
{children}
);
```
`updateValue` and `handleUpdateAsset` are already `useCallback`-stabilized; `updateTo`/`setMaxValueMode` are stable setters. Only the wrapping object is unstable.
**Fix** — wrap the value in `useMemo` keyed on the actual fields:
```tsx
const value = useMemo(
() => ({
asset,
chainId,
fromAccount,
from: fromAccount?.address as string,
maxValueMode,
to,
updateAsset: handleUpdateAsset,
updateTo,
updateValue,
value,
}),
[asset, chainId, fromAccount, maxValueMode, to, handleUpdateAsset, updateValue],
);
return {children};
```
This does not eliminate re-renders when a field actually changes (consumers still re-render when `value` changes); it removes the spurious fan-out when the provider re-renders for unrelated reasons.
### Threat Modeling Framework
N/A — performance-only change. No new data flow, trust boundary, or attack surface; behavior is preserved (memoization only changes referential identity of the context value).
### Acceptance Criteria
- The `SendContext.Provider` value is memoized so its identity is stable across provider re-renders that don't change any field.
- `yarn jest app/components/Views/confirmations/context/send-context/send-context.test.tsx` passes.
- A leaf consumer (e.g. recipient input) no longer re-renders when only the amount changes — verified with a render-count probe or the React DevTools Profiler.
### References
- File: `app/components/Views/confirmations/context/send-context/send-context.tsx:108`
- Source: MetaMask Mobile performance audit — finding `context-send-context-inline-value`
- Status: **UNVALIDATED** (validate the regression before/after with the steps above)
Contributor guide
Research direction
Start with app/components/Views/confirmations/context/send-context/send-context.tsx around line 108 and inspect the existing provider value and hook callbacks. Apply the stated memoization design, then run app/components/Views/confirmations/context/send-context/send-context.test.tsx. Verify the provider value remains stable when its fields do not change, and use a render-count probe or React DevTools Profiler to check the recipient consumer behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- frontend, mobile
- Issue type
- Refactor
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100