MetaMask / MetaMask/metamask-mobile
Move trackEvent/setState out of render phase in useIncompleteAssetEvent
- 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: Risky · Test safety net: Covered (app/components/UI/SimulationDetails/useSimulationMetrics.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/UI/SimulationDetails/useSimulationMetrics.ts:123`
### What is this about?
`useIncompleteAssetEvent` iterates `balanceChanges` directly in the render body (not inside an effect) and, per iteration, calls `trackEvent(...)` and `setProcessedAssets([...processedAssets, assetAddress])`. Calling `setState` and firing analytics during render is a React anti-pattern: it schedules re-renders mid-render, the `setProcessedAssets` reads stale `processedAssets` inside the loop (so a multi-asset render only records one asset then re-renders), and `trackEvent` fires as a render side effect.
**Why it matters**
Render-phase `setState` triggers extra render passes; combined with `useBalanceChanges` returning a fresh array each render (see `unstablehook-usebalancechanges-fresh-array.md`), this loop can fire repeatedly and emit duplicate or missed `INCOMPLETE_ASSET_DISPLAYED` analytics events. It is also unsafe under React concurrent rendering, where render bodies may run multiple times.
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Evidence**
`app/components/UI/SimulationDetails/useSimulationMetrics.ts:123`
```ts
for (const change of balanceChanges) {
...
trackEvent(
createEventBuilder(MetaMetricsEvents.INCOMPLETE_ASSET_DISPLAYED)
.addProperties({ ... })
.build(),
);
setProcessedAssets([...processedAssets, assetAddress]); // render-phase setState, stale closure
}
```
**Fix**
Move the loop into a `useEffect` keyed on `balanceChanges`/`displayNamesByAddress`. Accumulate the newly-processed addresses into a single array and call `setProcessedAssets(prev => [...prev, ...newlyProcessed])` once using the functional updater (avoids the stale-closure bug). Keep `trackEvent` calls inside the effect so they are committed side effects, not render side effects.
### 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/UI/SimulationDetails/useSimulationMetrics.test.ts` and confirm the incomplete-asset event assertions (count and properties) still pass.
- Add a test with two incomplete assets in one render and assert both events fire exactly once.
### References
- File: `app/components/UI/SimulationDetails/useSimulationMetrics.ts:123`
- Source: MetaMask Mobile performance audit — finding `unstablehook-useincompleteasset-render-side-effects`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- Status: **UNVALIDATED**
Contributor guide
Research direction
Start in app/components/UI/SimulationDetails/useSimulationMetrics.ts at line 123 and read the existing useSimulationMetrics test at app/components/UI/SimulationDetails/useSimulationMetrics.test.ts. Run the named Jest test before changing anything. Move the incomplete-asset work into an effect, preserve event properties, and add coverage confirming two incomplete assets each emit exactly once.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, testing-qa
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100