Skip to content

Add simple send e2e test#6669

Merged
whymarrh merged 2 commits intoMetaMask:e2e-2.0from
whymarrh:e2e-simple-send
Jul 18, 2019
Merged

Add simple send e2e test#6669
whymarrh merged 2 commits intoMetaMask:e2e-2.0from
whymarrh:e2e-simple-send

Conversation

@whymarrh
Copy link
Copy Markdown
Contributor

Closes #6548

This PR adds a e2e test using fixture data that exercises the send flow, sending 1 ETH from one account to another on a local test chain.

@whymarrh whymarrh force-pushed the e2e-simple-send branch 2 times, most recently from 20fb03a to 69fb1f5 Compare May 30, 2019 16:34
@whymarrh whymarrh marked this pull request as ready for review June 21, 2019 09:38
@whymarrh
Copy link
Copy Markdown
Contributor Author

@danjm this is ready for review—I've added it to CI but I'm still debugging it. It does pass locally so maybe we can leave out the CI job for now?

@danjm
Copy link
Copy Markdown
Contributor

danjm commented Jun 24, 2019

@whymarrh I agree we can leave out the CI for now.

@danjm
Copy link
Copy Markdown
Contributor

danjm commented Jun 24, 2019

Can you add some basic documentation for running these tests locally?

@danjm
Copy link
Copy Markdown
Contributor

danjm commented Jun 24, 2019

Do we need to commit all of these fixture files/

Copy link
Copy Markdown
Contributor

@danjm danjm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks fine. Suggested addition of documentation and questioned the committing of all the fixture files.

@whymarrh
Copy link
Copy Markdown
Contributor Author

Do we need to commit all of these fixture files

Whatever is needed for a test to run should be committed to the repo, yes. It's the best solution I can come up with, but I'm open to alternatives.

Can you add some basic documentation for running these tests locally?

I've added brief info for running the tests locally and for generating the fixture files.

@whymarrh whymarrh requested a review from Gudahtt July 10, 2019 16:50
Copy link
Copy Markdown
Contributor

@danjm danjm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@whymarrh whymarrh merged this pull request into MetaMask:e2e-2.0 Jul 18, 2019
@whymarrh whymarrh deleted the e2e-simple-send branch July 18, 2019 02:43
@whymarrh whymarrh mentioned this pull request Jul 18, 2019
github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
…bal cascading re-renders (#39312)

## **Description**

`getPendingApprovals` was creating a new array on every call via
`Object.values()`, breaking memoization in the Routes component and
triggering cascade re-renders across ~50-60 child components on every
state change.

**Changes:**
- Wrapped `getPendingApprovals` in `createSelector` to memoize the
`Object.values()` transformation
- Added helper function `getPendingApprovalsObject` to extract the
pendingApprovals object
- Returns stable reference when underlying `pendingApprovals` object is
unchanged

**Implementation:**
```typescript
// Before: new array every call
export function getPendingApprovals(state: ApprovalsMetaMaskState) {
  return Object.values(state.metamask.pendingApprovals ?? {});
}

// After: memoized transformation
const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) =>
  state.metamask.pendingApprovals ?? {};

export const getPendingApprovals = createSelector(
  getPendingApprovalsObject,
  (approvals) => Object.values(approvals),
);
```

**Testing:**
- Added unit tests verifying reference equality on repeated calls with
identical state
- Tests verify invalidation when pendingApprovals actually change
- All existing tests pass (664 selector tests across 31 suites)
- Covers edge cases (empty object, null/undefined values)

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by fixing approvals selector memoization

## **Related issues**

Fixes: https://github.com/MetaMask/MetaMask-planning/issues/6673
- Part of
[#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) —
Break Global Re-render Cascade

## **Manual testing steps**

1. Open MetaMask extension
2. Navigate to any page that triggers state changes (e.g., switching
networks, accounts)
3. Observe improved responsiveness compared to before (Routes component
and children no longer re-render unnecessarily)
4. Verify approvals functionality works correctly (no behavioral
changes)

## **Screenshots/Recordings**

### **Before**
N/A - Performance improvement (internal optimization, no UI changes)

### **After**
N/A - Performance improvement (internal optimization, no UI changes)

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Optimizes approvals selectors to avoid unnecessary renders by
stabilizing outputs and handling empty/null states.
> 
> - Wraps `getPendingApprovals` with `createSelector` and adds
`getPendingApprovalsObject` using `EMPTY_OBJECT` fallback
> - Converts `pendingApprovalsSortedSelector` to a memoized selector
that sorts a copied array
> - Leaves existing APIs intact while returning stable references when
`pendingApprovals` is unchanged
> - Adds unit tests validating memoization, change invalidation, and
empty/null behavior
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4984401. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
…bal cascading re-renders (#39312)

## **Description**

`getPendingApprovals` was creating a new array on every call via
`Object.values()`, breaking memoization in the Routes component and
triggering cascade re-renders across ~50-60 child components on every
state change.

**Changes:**
- Wrapped `getPendingApprovals` in `createSelector` to memoize the
`Object.values()` transformation
- Added helper function `getPendingApprovalsObject` to extract the
pendingApprovals object
- Returns stable reference when underlying `pendingApprovals` object is
unchanged

**Implementation:**
```typescript
// Before: new array every call
export function getPendingApprovals(state: ApprovalsMetaMaskState) {
  return Object.values(state.metamask.pendingApprovals ?? {});
}

// After: memoized transformation
const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) =>
  state.metamask.pendingApprovals ?? {};

export const getPendingApprovals = createSelector(
  getPendingApprovalsObject,
  (approvals) => Object.values(approvals),
);
```

**Testing:**
- Added unit tests verifying reference equality on repeated calls with
identical state
- Tests verify invalidation when pendingApprovals actually change
- All existing tests pass (664 selector tests across 31 suites)
- Covers edge cases (empty object, null/undefined values)

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by fixing approvals selector memoization

## **Related issues**

Fixes: https://github.com/MetaMask/MetaMask-planning/issues/6673
- Part of
[#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) —
Break Global Re-render Cascade

## **Manual testing steps**

1. Open MetaMask extension
2. Navigate to any page that triggers state changes (e.g., switching
networks, accounts)
3. Observe improved responsiveness compared to before (Routes component
and children no longer re-render unnecessarily)
4. Verify approvals functionality works correctly (no behavioral
changes)

## **Screenshots/Recordings**

### **Before**
N/A - Performance improvement (internal optimization, no UI changes)

### **After**
N/A - Performance improvement (internal optimization, no UI changes)

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Optimizes approvals selectors to avoid unnecessary renders by
stabilizing outputs and handling empty/null states.
> 
> - Wraps `getPendingApprovals` with `createSelector` and adds
`getPendingApprovalsObject` using `EMPTY_OBJECT` fallback
> - Converts `pendingApprovalsSortedSelector` to a memoized selector
that sorts a copied array
> - Leaves existing APIs intact while returning stable references when
`pendingApprovals` is unchanged
> - Adds unit tests validating memoization, change invalidation, and
empty/null behavior
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4984401. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
github-merge-queue bot pushed a commit that referenced this pull request Jan 23, 2026
… to prevent global cascading re-renders (#39313)

## **Description**

Fixed `pendingConfirmationsSortedSelector` to use proper memoization
with `createSelector`. This selector was creating new arrays on every
call, breaking memoization and triggering cascade re-renders throughout
the confirmation flow and Routes components.

**What is the reason for the change?**
The selector was implemented as a plain function that returned new array
references on every invocation (via `.filter()` and `.sort()`), causing
unnecessary re-renders in all dependent components even when the
underlying state hadn't changed.

**What is the improvement/solution?**
Wrapped the selector with `createSelector` from reselect to memoize the
transformation. Now the selector only recalculates when
`getPendingApprovals` returns a different reference, preventing cascade
re-renders and improving performance across the confirmation flow.

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by fixing confirmations selector memoization

## **Related issues**

Fixes: Part of P0 effort to break global re-render cascade (#6669)

## **Manual testing steps**

1. This is a performance optimization with no visible UI changes
2. The memoization behavior is verified through unit tests
3. All existing confirmation flow functionality remains unchanged

## **Screenshots/Recordings**

N/A - This is a performance optimization with no visual changes. The
improvement is verified through unit tests that confirm the selector
returns the same reference when state is unchanged.

### **Before**

Selector created new arrays on every call, causing unnecessary
re-renders:
```typescript
export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) {
  return getPendingApprovals(state)
    .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType))
    .sort((a1, a2) => a1.time - a2.time);
}
```

### **After**

Selector uses memoization to return stable references:
```typescript
export const pendingConfirmationsSortedSelector = createSelector(
  getPendingApprovals,
  (approvals) =>
    approvals
      .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType))
      .sort((a1, a2) => a1.time - a2.time),
);
```

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>[P0] Fix `pendingConfirmationsSortedSelector`
(unmemoized)</issue_title>
> <issue_description>## Summary
> 
> Fix `pendingConfirmationsSortedSelector` to use proper memoization.
This is a **P0 critical fix** required to break the global re-render
cascade.
> 
> **Parent Epic:**
[#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) —
Break Global Re-render Cascade
> **Depends on:** Fix for `getPendingApprovals` (#6673)
> 
> ## Problem
> 
> `pendingConfirmationsSortedSelector` is a plain function that creates
2 new arrays on every call:
> 
> ```typescript
> // ui/pages/confirmations/selectors/confirm.ts
> export function pendingConfirmationsSortedSelector(state:
ConfirmMetamaskState) {
>   return getPendingApprovals(state)
> .filter(({ type }) => ConfirmationApprovalTypes.includes(type as
ApprovalType)) // ❌ NEW ARRAY
>     .sort((a1, a2) => a1.time - a2.time);  // ❌ NEW ARRAY
> }
> ```
> 
> **Used in Routes** to determine pending confirmation count and
navigation.
> 
> This breaks memoization and causes cascade re-renders across all
children on every state change.
> 
> ## Solution
> 
> Wrap in `createSelector` to memoize the transformation:
> 
> ```typescript
> export const pendingConfirmationsSortedSelector = createSelector(
>   getPendingApprovals,
>   (approvals) =>
>     approvals
> .filter(({ type }) => ConfirmationApprovalTypes.includes(type as
ApprovalType))
>       .sort((a1, a2) => a1.time - a2.time),
> );
> ```
> 
> ## Files
> 
> - `ui/pages/confirmations/selectors/confirm.ts`
> 
> ## Acceptance Criteria
> 
> - [ ] Selector returns same reference when underlying state unchanged
> - [ ] Unit test verifies memoization
> - [ ] No regression in existing tests
> 
> ## Estimated Effort
> 
> 0.5 hours
> 
> ## Labels
> 
> `perf`, `selector`, `P0`, `team-confirmations`</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes MetaMask/MetaMask-planning#6674

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Improves selector performance and adds tests around confirmation
sorting/filtering.
> 
> - **Memoizes** `pendingConfirmationsSortedSelector` with `reselect`
using `getPendingApprovals`, preserving sort-by-time and
confirmation-type filtering while returning stable references
> - **Tests**: verify sorted output, memoization (same reference when
unchanged, new reference on state change), and filtering of
non-confirmation approvals; `oldestPendingConfirmationSelector` behavior
validated
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
88f2be0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
wantedsystem pushed a commit that referenced this pull request Jan 27, 2026
…bal cascading re-renders (#39312)

## **Description**

`getPendingApprovals` was creating a new array on every call via
`Object.values()`, breaking memoization in the Routes component and
triggering cascade re-renders across ~50-60 child components on every
state change.

**Changes:**
- Wrapped `getPendingApprovals` in `createSelector` to memoize the
`Object.values()` transformation
- Added helper function `getPendingApprovalsObject` to extract the
pendingApprovals object
- Returns stable reference when underlying `pendingApprovals` object is
unchanged

**Implementation:**
```typescript
// Before: new array every call
export function getPendingApprovals(state: ApprovalsMetaMaskState) {
  return Object.values(state.metamask.pendingApprovals ?? {});
}

// After: memoized transformation
const getPendingApprovalsObject = (state: ApprovalsMetaMaskState) =>
  state.metamask.pendingApprovals ?? {};

export const getPendingApprovals = createSelector(
  getPendingApprovalsObject,
  (approvals) => Object.values(approvals),
);
```

**Testing:**
- Added unit tests verifying reference equality on repeated calls with
identical state
- Tests verify invalidation when pendingApprovals actually change
- All existing tests pass (664 selector tests across 31 suites)
- Covers edge cases (empty object, null/undefined values)

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by fixing approvals selector memoization

## **Related issues**

Fixes: MetaMask/MetaMask-planning#6673
- Part of
[#6669](MetaMask/MetaMask-planning#6669) —
Break Global Re-render Cascade

## **Manual testing steps**

1. Open MetaMask extension
2. Navigate to any page that triggers state changes (e.g., switching
networks, accounts)
3. Observe improved responsiveness compared to before (Routes component
and children no longer re-render unnecessarily)
4. Verify approvals functionality works correctly (no behavioral
changes)

## **Screenshots/Recordings**

### **Before**
N/A - Performance improvement (internal optimization, no UI changes)

### **After**
N/A - Performance improvement (internal optimization, no UI changes)

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Optimizes approvals selectors to avoid unnecessary renders by
stabilizing outputs and handling empty/null states.
> 
> - Wraps `getPendingApprovals` with `createSelector` and adds
`getPendingApprovalsObject` using `EMPTY_OBJECT` fallback
> - Converts `pendingApprovalsSortedSelector` to a memoized selector
that sorts a copied array
> - Leaves existing APIs intact while returning stable references when
`pendingApprovals` is unchanged
> - Adds unit tests validating memoization, change invalidation, and
empty/null behavior
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4984401. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
wantedsystem pushed a commit that referenced this pull request Jan 27, 2026
… to prevent global cascading re-renders (#39313)

## **Description**

Fixed `pendingConfirmationsSortedSelector` to use proper memoization
with `createSelector`. This selector was creating new arrays on every
call, breaking memoization and triggering cascade re-renders throughout
the confirmation flow and Routes components.

**What is the reason for the change?**
The selector was implemented as a plain function that returned new array
references on every invocation (via `.filter()` and `.sort()`), causing
unnecessary re-renders in all dependent components even when the
underlying state hadn't changed.

**What is the improvement/solution?**
Wrapped the selector with `createSelector` from reselect to memoize the
transformation. Now the selector only recalculates when
`getPendingApprovals` returns a different reference, preventing cascade
re-renders and improving performance across the confirmation flow.

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by fixing confirmations selector memoization

## **Related issues**

Fixes: Part of P0 effort to break global re-render cascade (#6669)

## **Manual testing steps**

1. This is a performance optimization with no visible UI changes
2. The memoization behavior is verified through unit tests
3. All existing confirmation flow functionality remains unchanged

## **Screenshots/Recordings**

N/A - This is a performance optimization with no visual changes. The
improvement is verified through unit tests that confirm the selector
returns the same reference when state is unchanged.

### **Before**

Selector created new arrays on every call, causing unnecessary
re-renders:
```typescript
export function pendingConfirmationsSortedSelector(state: ConfirmMetamaskState) {
  return getPendingApprovals(state)
    .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType))
    .sort((a1, a2) => a1.time - a2.time);
}
```

### **After**

Selector uses memoization to return stable references:
```typescript
export const pendingConfirmationsSortedSelector = createSelector(
  getPendingApprovals,
  (approvals) =>
    approvals
      .filter(({ type }) => ConfirmationApprovalTypes.includes(type as ApprovalType))
      .sort((a1, a2) => a1.time - a2.time),
);
```

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>[P0] Fix `pendingConfirmationsSortedSelector`
(unmemoized)</issue_title>
> <issue_description>## Summary
> 
> Fix `pendingConfirmationsSortedSelector` to use proper memoization.
This is a **P0 critical fix** required to break the global re-render
cascade.
> 
> **Parent Epic:**
[#6669](MetaMask/MetaMask-planning#6669) —
Break Global Re-render Cascade
> **Depends on:** Fix for `getPendingApprovals` (#6673)
> 
> ## Problem
> 
> `pendingConfirmationsSortedSelector` is a plain function that creates
2 new arrays on every call:
> 
> ```typescript
> // ui/pages/confirmations/selectors/confirm.ts
> export function pendingConfirmationsSortedSelector(state:
ConfirmMetamaskState) {
>   return getPendingApprovals(state)
> .filter(({ type }) => ConfirmationApprovalTypes.includes(type as
ApprovalType)) // ❌ NEW ARRAY
>     .sort((a1, a2) => a1.time - a2.time);  // ❌ NEW ARRAY
> }
> ```
> 
> **Used in Routes** to determine pending confirmation count and
navigation.
> 
> This breaks memoization and causes cascade re-renders across all
children on every state change.
> 
> ## Solution
> 
> Wrap in `createSelector` to memoize the transformation:
> 
> ```typescript
> export const pendingConfirmationsSortedSelector = createSelector(
>   getPendingApprovals,
>   (approvals) =>
>     approvals
> .filter(({ type }) => ConfirmationApprovalTypes.includes(type as
ApprovalType))
>       .sort((a1, a2) => a1.time - a2.time),
> );
> ```
> 
> ## Files
> 
> - `ui/pages/confirmations/selectors/confirm.ts`
> 
> ## Acceptance Criteria
> 
> - [ ] Selector returns same reference when underlying state unchanged
> - [ ] Unit test verifies memoization
> - [ ] No regression in existing tests
> 
> ## Estimated Effort
> 
> 0.5 hours
> 
> ## Labels
> 
> `perf`, `selector`, `P0`, `team-confirmations`</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes MetaMask/MetaMask-planning#6674

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Improves selector performance and adds tests around confirmation
sorting/filtering.
> 
> - **Memoizes** `pendingConfirmationsSortedSelector` with `reselect`
using `getPendingApprovals`, preserving sort-by-time and
confirmation-type filtering while returning stable references
> - **Tests**: verify sorted output, memoization (same reference when
unchanged, new reference on state change), and filtering of
non-confirmation approvals; `oldestPendingConfirmationSelector` behavior
validated
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
88f2be0. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
github-merge-queue bot pushed a commit that referenced this pull request Jan 30, 2026
…eferences (#39311)

## **Description**

This PR fixes a critical performance issue where `useParams()` and
`useLocation()` hooks return new object references on every render, even
when values are identical. The `withRouterHooks` HOC was passing these
unstable references directly to wrapped components, breaking
`React.memo` optimization and triggering cascade re-renders across ~40
components per cycle, with multiple cascade cycles (5+) per user action.

**What is the reason for the change?**
The unstable object references from router hooks were causing
unnecessary re-renders throughout the application, significantly slowing
down all user actions.

**What is the improvement/solution?**
Added memoization to stabilize `params` and `location` prop references:
- For `params`: Track keys and values as separate string primitives to
detect actual content changes
- For `location`: Track individual properties (pathname, search, hash,
state) to detect route changes
- Both use `useMemo` to return the same object reference when content
hasn't changed

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by stabilizing props references used in routes

## **Related issues**

Part of Break Global Re-render Cascade epic

## **Manual testing steps**

1. Use the extension and navigate between different routes
2. Perform user actions that trigger route changes (e.g., sending
transactions, switching accounts)
3. With performance profiling or React DevTools, observe that wrapped
components using `withRouterHooks` no longer re-render unnecessarily
when route values haven't changed
4. Verify all existing functionality works as expected (navigation,
routing, etc.)

## **Screenshots/Recordings**

N/A - This is a performance optimization with no visible UI changes. The
improvement is observable through React DevTools Profiler showing
reduced re-render counts.

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>[P0] Memoize `withRouterHooks` HOC (params/location
stability)</issue_title>
> <issue_description>## Summary
> 
> Stabilize object references from `useParams()` and `useLocation()` in
`withRouterHooks` HOC. This is a **P0 critical fix** required to break
the global re-render cascade.
> 
> **Parent Epic:**
[#6669](https://github.com/MetaMask/MetaMask-planning/issues/6669) —
Break Global Re-render Cascade
> 
> ## Problem
> 
> `withRouterHooks` HOC passes `useParams()` and `useLocation()` results
directly to wrapped components. These hooks return **new object
references on every render** even when the values are identical,
breaking `React.memo` and causing cascade re-renders.
> 
> **WDYR Analysis shows:**
> - `props.params` flagged as "different objects that are equal by
value"
> - This triggers on ~40 components per cascade cycle
> - Multiple cascade cycles per user action (5+)
> 
> ## Current Code
> 
> ```typescript
> //
ui/helpers/higher-order-components/with-router/with-router.component.js
> const ComponentWithRouterHooks = (props) => {
>   const hookNavigate = useNavigate();
>   const hookLocation = useLocation();
>   const hookParams = useParams();
> 
>   return (
>     <WrappedComponent
>       navigate={props.navigate ?? hookNavigate}
>       location={props.location ?? hookLocation}
>       params={props.params ?? hookParams}  // New object every render!
>     />
>   );
> };
> ```
> 
> ## Solution
> 
> Memoize router props to maintain stable references:
> 
> ```typescript
> import { useMemo } from 'react';
> 
> const ComponentWithRouterHooks = (props) => {
>   const hookNavigate = useNavigate();
>   const hookLocation = useLocation();
>   const hookParams = useParams();
> 
>   // Stabilize params object
>   const stableParams = useMemo(
>     () => props.params ?? hookParams,
>     [props.params, JSON.stringify(hookParams)],
>   );
> 
>   // Stabilize location object
>   const stableLocation = useMemo(
>     () => props.location ?? hookLocation,
>     [
>       props.location,
>       hookLocation.pathname,
>       hookLocation.search,
>       hookLocation.hash,
>       hookLocation.state,
>     ],
>   );
> 
>   return (
>     <WrappedComponent
>       {...props}
>       navigate={props.navigate ?? hookNavigate}
>       location={stableLocation}
>       params={stableParams}
>     />
>   );
> };
> ```
> 
> ## Files
> 
> -
`ui/helpers/higher-order-components/with-router/with-router.component.js`
> 
> ## Acceptance Criteria
> 
> - [ ] `params` and `location` props maintain stable references when
values unchanged
> - [ ] Unit tests verify memoization works correctly
> - [ ] No regression in routing behavior
> - [ ] WDYR no longer flags "different objects that are equal by value"
for params/location
> 
> ## Estimated Effort
> 
> 2 hours
> 
> ## Labels
> 
> `perf`, `HOC`, `P0`, `team-extension-platform`</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes MetaMask/MetaMask-planning#6671

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches a widely used routing HOC and changes how `location` updates
propagate (ignoring `location.key`), which could affect components that
implicitly relied on key-based updates. Logic is straightforward and
covered by new unit tests, but regressions would be broadly visible in
navigation-related UI.
> 
> **Overview**
> `withRouterHooks` now memoizes router-derived props so wrapped
components receive **referentially stable** `params` and `location`
values when their meaningful contents haven’t changed.
> 
> This introduces a new `useShallowEqualityCheck` hook for
shallow-reference stabilization (used for `params`) and adds
`useLocationStable` to keep `location` stable while *intentionally
ignoring* `location.key` changes. Tests were expanded to assert
memoization behavior, precedence of explicitly passed
`params`/`location` props, and to cover edge cases like comma-containing
param values and same-path navigations where only `location.key`
changes.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7fd9991. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
georgewrmarshall pushed a commit that referenced this pull request Feb 2, 2026
…eferences (#39311)

## **Description**

This PR fixes a critical performance issue where `useParams()` and
`useLocation()` hooks return new object references on every render, even
when values are identical. The `withRouterHooks` HOC was passing these
unstable references directly to wrapped components, breaking
`React.memo` optimization and triggering cascade re-renders across ~40
components per cycle, with multiple cascade cycles (5+) per user action.

**What is the reason for the change?**
The unstable object references from router hooks were causing
unnecessary re-renders throughout the application, significantly slowing
down all user actions.

**What is the improvement/solution?**
Added memoization to stabilize `params` and `location` prop references:
- For `params`: Track keys and values as separate string primitives to
detect actual content changes
- For `location`: Track individual properties (pathname, search, hash,
state) to detect route changes
- Both use `useMemo` to return the same object reference when content
hasn't changed

## **Changelog**

CHANGELOG entry: Fixed critical performance issue slowing down all user
actions by stabilizing props references used in routes

## **Related issues**

Part of Break Global Re-render Cascade epic

## **Manual testing steps**

1. Use the extension and navigate between different routes
2. Perform user actions that trigger route changes (e.g., sending
transactions, switching accounts)
3. With performance profiling or React DevTools, observe that wrapped
components using `withRouterHooks` no longer re-render unnecessarily
when route values haven't changed
4. Verify all existing functionality works as expected (navigation,
routing, etc.)

## **Screenshots/Recordings**

N/A - This is a performance optimization with no visible UI changes. The
improvement is observable through React DevTools Profiler showing
reduced re-render counts.

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>[P0] Memoize `withRouterHooks` HOC (params/location
stability)</issue_title>
> <issue_description>## Summary
> 
> Stabilize object references from `useParams()` and `useLocation()` in
`withRouterHooks` HOC. This is a **P0 critical fix** required to break
the global re-render cascade.
> 
> **Parent Epic:**
[#6669](MetaMask/MetaMask-planning#6669) —
Break Global Re-render Cascade
> 
> ## Problem
> 
> `withRouterHooks` HOC passes `useParams()` and `useLocation()` results
directly to wrapped components. These hooks return **new object
references on every render** even when the values are identical,
breaking `React.memo` and causing cascade re-renders.
> 
> **WDYR Analysis shows:**
> - `props.params` flagged as "different objects that are equal by
value"
> - This triggers on ~40 components per cascade cycle
> - Multiple cascade cycles per user action (5+)
> 
> ## Current Code
> 
> ```typescript
> //
ui/helpers/higher-order-components/with-router/with-router.component.js
> const ComponentWithRouterHooks = (props) => {
>   const hookNavigate = useNavigate();
>   const hookLocation = useLocation();
>   const hookParams = useParams();
> 
>   return (
>     <WrappedComponent
>       navigate={props.navigate ?? hookNavigate}
>       location={props.location ?? hookLocation}
>       params={props.params ?? hookParams}  // New object every render!
>     />
>   );
> };
> ```
> 
> ## Solution
> 
> Memoize router props to maintain stable references:
> 
> ```typescript
> import { useMemo } from 'react';
> 
> const ComponentWithRouterHooks = (props) => {
>   const hookNavigate = useNavigate();
>   const hookLocation = useLocation();
>   const hookParams = useParams();
> 
>   // Stabilize params object
>   const stableParams = useMemo(
>     () => props.params ?? hookParams,
>     [props.params, JSON.stringify(hookParams)],
>   );
> 
>   // Stabilize location object
>   const stableLocation = useMemo(
>     () => props.location ?? hookLocation,
>     [
>       props.location,
>       hookLocation.pathname,
>       hookLocation.search,
>       hookLocation.hash,
>       hookLocation.state,
>     ],
>   );
> 
>   return (
>     <WrappedComponent
>       {...props}
>       navigate={props.navigate ?? hookNavigate}
>       location={stableLocation}
>       params={stableParams}
>     />
>   );
> };
> ```
> 
> ## Files
> 
> -
`ui/helpers/higher-order-components/with-router/with-router.component.js`
> 
> ## Acceptance Criteria
> 
> - [ ] `params` and `location` props maintain stable references when
values unchanged
> - [ ] Unit tests verify memoization works correctly
> - [ ] No regression in routing behavior
> - [ ] WDYR no longer flags "different objects that are equal by value"
for params/location
> 
> ## Estimated Effort
> 
> 2 hours
> 
> ## Labels
> 
> `perf`, `HOC`, `P0`, `team-extension-platform`</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes MetaMask/MetaMask-planning#6671

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches a widely used routing HOC and changes how `location` updates
propagate (ignoring `location.key`), which could affect components that
implicitly relied on key-based updates. Logic is straightforward and
covered by new unit tests, but regressions would be broadly visible in
navigation-related UI.
> 
> **Overview**
> `withRouterHooks` now memoizes router-derived props so wrapped
components receive **referentially stable** `params` and `location`
values when their meaningful contents haven’t changed.
> 
> This introduces a new `useShallowEqualityCheck` hook for
shallow-reference stabilization (used for `params`) and adds
`useLocationStable` to keep `location` stable while *intentionally
ignoring* `location.key` changes. Tests were expanded to assert
memoization behavior, precedence of explicitly passed
`params`/`location` props, and to cover edge cases like comma-containing
param values and same-path navigations where only `location.key`
changes.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7fd9991. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jongsun Suh <jongsun.suh@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants