MetaMask / MetaMask/metamask-mobile

Move trackEvent/setState out of render phase in useIncompleteAssetEvent

Open
#31,380 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev2 size-S 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: 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.