Skip to content

Commit 28ff526

Browse files
authored
refactor: accumulator spread in reduce (#37435)
## **Description** Improve readability and performance Spreading the accumulator can generally be [slower](https://www.richsnapp.com/article/2019/06-09-reduce-spread-anti-pattern) than a simple for loop, not to mention less readable [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/37435?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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] > Refactors multiple reducers using spread into explicit loops/Object.assign for building maps, arrays, and initial state across controllers, selectors, UI, tests, and constants. > > - **Core/Controllers** > - `app/scripts/constants/sentry-state.ts`: Replace `reduce` + spread with loop and `Object.assign` to build `flattenedBackgroundStateMask`. > - `app/scripts/controller-init/assets/network-enablement-controller-init.ts`: Build `allEnabledNetworks` via loop and `Object.assign`. > - `app/scripts/controllers/metametrics-controller.ts`: Rework `#buildValidTraits` to use `for...of` accumulation; preserve validation and return shape. > - `app/scripts/lib/ComposableObservableStore.js`: Use `Object.assign` when merging child states in `getFlatState`. > - **Shared/Constants** > - `shared/constants/tokens.js`: Build `STATIC_MAINNET_TOKEN_LIST` via loop; switch to named export after declaration. > - **Selectors** > - `shared/modules/selectors/networks.ts`: Build `clientIdsByChain` via loop; in `getNetworksByScopes`, use `concat` instead of array spread. > - `ui/selectors/multichain/networks.ts`: Construct EVM multichain config map via loop. > - `ui/selectors/selectors.js`: Mutate accumulator object in accounts reduce instead of returning new object each iteration. > - **UI Components** > - `ui/components/app/contact-list/contact-list.component.js`: Group contacts by letter using explicit loops and pre-allocated buckets. > - `ui/components/multichain/.../asset-picker-modal-network.tsx`: Initialize and update `checkedChainIds` via loops; rework "select all" toggle to build a new map. > - **Tests** > - `test/e2e/fixture-builder.js`: Build `optionalScopes` for multichain test dapp using a loop. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 41a2b7f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a90ccb4 commit 28ff526

File tree

11 files changed

+133
-149
lines changed

11 files changed

+133
-149
lines changed

app/scripts/constants/sentry-state.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,14 +396,11 @@ export const SENTRY_BACKGROUND_STATE = {
396396
},
397397
};
398398

399-
const flattenedBackgroundStateMask = Object.values(
400-
SENTRY_BACKGROUND_STATE,
401-
).reduce((partialBackgroundState, controllerState: object) => {
402-
return {
403-
...partialBackgroundState,
404-
...controllerState,
405-
};
406-
}, {});
399+
const flattenedBackgroundStateMask: Record<string, unknown> = {};
400+
401+
for (const controllerState of Object.values(SENTRY_BACKGROUND_STATE)) {
402+
Object.assign(flattenedBackgroundStateMask, controllerState);
403+
}
407404

408405
// This describes the subset of Redux state attached to errors sent to Sentry
409406
// These properties have some potential to be useful for debugging, and they do

app/scripts/controller-init/assets/network-enablement-controller-init.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,11 @@ export const NetworkEnablementControllerInit: ControllerInitFunction<
187187
);
188188
///: END:ONLY_INCLUDE_IF
189189

190-
const allEnabledNetworks = Object.values(
191-
controller.state.enabledNetworkMap,
192-
).reduce((acc, curr) => {
193-
return { ...acc, ...curr };
194-
}, {});
190+
const allEnabledNetworks = {};
191+
192+
for (const network of Object.values(controller.state.enabledNetworkMap)) {
193+
Object.assign(allEnabledNetworks, network);
194+
}
195195

196196
if (Object.keys(allEnabledNetworks).length === 1) {
197197
const chainId = Object.keys(allEnabledNetworks)[0];

app/scripts/controllers/metametrics-controller.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,27 +1471,21 @@ export default class MetaMetricsController extends BaseController<
14711471
#buildValidTraits(
14721472
userTraits: Partial<MetaMetricsUserTraits>,
14731473
): MetaMetricsUserTraits {
1474-
return Object.entries(userTraits).reduce(
1475-
(validTraits: MetaMetricsUserTraits, [key, value]) => {
1476-
if (this.#isValidTraitDate(value)) {
1477-
return {
1478-
...validTraits,
1479-
[key]: value.toISOString(),
1480-
};
1481-
} else if (this.#isValidTrait(value)) {
1482-
return {
1483-
...validTraits,
1484-
[key]: value,
1485-
};
1486-
}
1487-
1474+
const validTraits: Record<string, string> = {};
1475+
1476+
for (const [key, value] of Object.entries(userTraits)) {
1477+
if (this.#isValidTraitDate(value)) {
1478+
validTraits[key] = value.toISOString();
1479+
} else if (this.#isValidTrait(value)) {
1480+
(validTraits as Record<string, typeof value>)[key] = value;
1481+
} else {
14881482
console.warn(
14891483
`MetaMetricsController: "${key}" value is not a valid trait type`,
14901484
);
1491-
return validTraits;
1492-
},
1493-
{},
1494-
);
1485+
}
1486+
}
1487+
1488+
return validTraits;
14951489
}
14961490

14971491
/**

app/scripts/lib/ComposableObservableStore.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ export default class ComposableObservableStore extends ObservableStore {
107107
if (!this.config) {
108108
return {};
109109
}
110-
let flatState = {};
110+
const flatState = {};
111111
for (const key of Object.keys(this.config)) {
112112
const controller = this.config[key];
113113
const state = controller.getState
114114
? controller.getState()
115115
: controller.state;
116-
flatState = { ...flatState, ...state };
116+
Object.assign(flatState, state);
117117
}
118118
return flatState;
119119
}

shared/constants/tokens.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,19 @@ export const LISTED_CONTRACT_ADDRESSES = Object.keys(contractMap).map(
2525
* @property {boolean} [isERC721] - True when the asset is a ERC721 token.
2626
*/
2727

28-
export const STATIC_MAINNET_TOKEN_LIST = Object.keys(contractMap).reduce(
29-
(acc, base) => {
30-
const { logo, ...tokenMetadata } = contractMap[base];
31-
return {
32-
...acc,
33-
[base.toLowerCase()]: {
34-
...tokenMetadata,
35-
address: base.toLowerCase(),
36-
iconUrl: `images/contract/${logo}`,
37-
aggregators: [],
38-
},
39-
};
40-
},
41-
{},
42-
);
28+
const STATIC_MAINNET_TOKEN_LIST = {};
29+
30+
for (const base of Object.keys(contractMap)) {
31+
const { logo, ...tokenMetadata } = contractMap[base];
32+
STATIC_MAINNET_TOKEN_LIST[base.toLowerCase()] = {
33+
...tokenMetadata,
34+
address: base.toLowerCase(),
35+
iconUrl: `images/contract/${logo}`,
36+
aggregators: [],
37+
};
38+
}
39+
40+
export { STATIC_MAINNET_TOKEN_LIST };
4341

4442
export const TOKEN_API_METASWAP_CODEFI_URL =
4543
'https://token.api.cx.metamask.io/tokens/';

shared/modules/selectors/networks.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,18 @@ export const getNetworkConfigurationsByChainId = (
7272
export const selectDefaultNetworkClientIdsByChainId = createSelector(
7373
getNetworkConfigurationsByChainId,
7474
(networkConfigurationsByChainId) => {
75-
return Object.entries(networkConfigurationsByChainId).reduce<
76-
Record<Hex, NetworkClientId>
77-
>(
78-
(obj, [chainId, networkConfiguration]) => ({
79-
...obj,
80-
[chainId]:
81-
networkConfiguration.rpcEndpoints[
82-
networkConfiguration.defaultRpcEndpointIndex
83-
].networkClientId,
84-
}),
85-
{},
86-
);
75+
const clientIdsByChain: Record<Hex, NetworkClientId> = {};
76+
77+
for (const [chainId, networkConfiguration] of Object.entries(
78+
networkConfigurationsByChainId,
79+
)) {
80+
clientIdsByChain[chainId as Hex] =
81+
networkConfiguration.rpcEndpoints[
82+
networkConfiguration.defaultRpcEndpointIndex
83+
].networkClientId;
84+
}
85+
86+
return clientIdsByChain;
8787
},
8888
);
8989

@@ -366,21 +366,18 @@ export const getNetworksByScopes = createSelector(
366366
name: network.name,
367367
}));
368368

369-
return [...result, ...evmNetworks];
369+
return result.concat(evmNetworks);
370370
}
371371

372372
const matchingNetwork = nonTestNetworks.find(
373373
(network) => network.caipChainId === scope,
374374
);
375375

376376
if (matchingNetwork) {
377-
return [
378-
...result,
379-
{
380-
chainId: matchingNetwork.chainId,
381-
name: matchingNetwork.name,
382-
},
383-
];
377+
return result.concat({
378+
chainId: matchingNetwork.chainId,
379+
name: matchingNetwork.name,
380+
});
384381
}
385382

386383
return result;

test/e2e/fixture-builder.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -586,18 +586,16 @@ class FixtureBuilder {
586586
withPermissionControllerConnectedToMultichainTestDappWithTwoAccounts({
587587
scopes = ['eip155:1337'],
588588
}) {
589-
const optionalScopes = scopes
590-
.map((scope) => ({
591-
[scope]: {
592-
accounts: [
593-
`${scope}:0x5cfe73b6021e818b776b421b1c4db2474086a7e1`,
594-
`${scope}:0x09781764c08de8ca82e156bbf156a3ca217c7950`,
595-
],
596-
},
597-
}))
598-
.reduce((acc, curr) => {
599-
return { ...acc, ...curr };
600-
}, {});
589+
const optionalScopes = {};
590+
591+
for (const scope of scopes) {
592+
optionalScopes[scope] = {
593+
accounts: [
594+
`${scope}:0x5cfe73b6021e818b776b421b1c4db2474086a7e1`,
595+
`${scope}:0x09781764c08de8ca82e156bbf156a3ca217c7950`,
596+
],
597+
};
598+
}
601599

602600
const subjects = {
603601
[DAPP_URL]: {

ui/components/app/contact-list/contact-list.component.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,24 @@ export default class ContactList extends PureComponent {
7575
internalAccounts,
7676
);
7777

78-
const unsortedContactsByLetter = searchForContacts().reduce(
79-
(obj, contact) => {
80-
const firstLetter = contact.name[0].toUpperCase();
81-
82-
const isDuplicate =
83-
(duplicateContactMap.get(contact.name.trim().toLowerCase()) ?? [])
84-
.length > 1;
85-
86-
return {
87-
...obj,
88-
[firstLetter]: [
89-
...(obj[firstLetter] || []),
90-
{
91-
...contact,
92-
isDuplicate,
93-
},
94-
],
95-
};
96-
},
97-
{},
98-
);
78+
const unsortedContactsByLetter = {};
79+
80+
for (const contact of searchForContacts()) {
81+
const firstLetter = contact.name[0].toUpperCase();
82+
83+
const isDuplicate =
84+
(duplicateContactMap.get(contact.name.trim().toLowerCase()) ?? [])
85+
.length > 1;
86+
87+
if (!unsortedContactsByLetter[firstLetter]) {
88+
unsortedContactsByLetter[firstLetter] = [];
89+
}
90+
91+
unsortedContactsByLetter[firstLetter].push({
92+
...contact,
93+
isDuplicate,
94+
});
95+
}
9996

10097
const letters = Object.keys(unsortedContactsByLetter).sort();
10198

ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-network.tsx

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -124,32 +124,35 @@ export const AssetPickerModalNetwork = ({
124124
// Initialized with the selectedChainIds if provided
125125
const [checkedChainIds, setCheckedChainIds] = useState<
126126
Record<string, boolean>
127-
>(
128-
networksList?.reduce(
129-
(acc, { chainId }) => ({
130-
...acc,
131-
[chainId]: selectedChainIds
132-
? selectedChainIds.includes(chainId)
133-
: false,
134-
}),
135-
{},
136-
) ?? {},
137-
);
127+
>(() => {
128+
if (!networksList) {
129+
return {};
130+
}
131+
132+
const initialState: Record<string, boolean> = {};
133+
134+
for (const { chainId } of networksList) {
135+
initialState[chainId] = selectedChainIds
136+
? selectedChainIds.includes(chainId)
137+
: false;
138+
}
139+
140+
return initialState;
141+
});
138142

139143
// Reset checkedChainIds if selectedChainIds change in parent component
140144
useEffect(() => {
141-
networksList &&
142-
setCheckedChainIds(
143-
networksList.reduce(
144-
(acc, { chainId }) => ({
145-
...acc,
146-
[chainId]: selectedChainIds
147-
? selectedChainIds.includes(chainId)
148-
: false,
149-
}),
150-
{},
151-
),
152-
);
145+
if (networksList) {
146+
const updatedState: Record<string, boolean> = {};
147+
148+
for (const { chainId } of networksList) {
149+
updatedState[chainId] = selectedChainIds
150+
? selectedChainIds.includes(chainId)
151+
: false;
152+
}
153+
154+
setCheckedChainIds(updatedState);
155+
}
153156
}, [networksList, selectedChainIds]);
154157

155158
const handleToggleNetwork = useCallback((chainId: string) => {
@@ -161,15 +164,14 @@ export const AssetPickerModalNetwork = ({
161164

162165
// Toggles all networks to be checked or unchecked
163166
const handleToggleAllNetworks = useCallback(() => {
164-
setCheckedChainIds(
165-
Object.keys(checkedChainIds)?.reduce(
166-
(agg, chainId) => ({
167-
...agg,
168-
[chainId]: !Object.values(checkedChainIds).every((v) => v),
169-
}),
170-
{},
171-
),
172-
);
167+
const toggledState: Record<string, boolean> = {};
168+
const allChecked = Object.values(checkedChainIds).every((v) => v);
169+
170+
for (const chainId of Object.keys(checkedChainIds)) {
171+
toggledState[chainId] = !allChecked;
172+
}
173+
174+
setCheckedChainIds(toggledState);
173175
}, [checkedChainIds]);
174176

175177
return (

ui/selectors/multichain/networks.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,21 @@ export const getMultichainNetworkConfigurationsByChainId =
198198
// There's a fallback for EVM network names/nicknames, in case the network
199199
// does not have a name/nickname the fallback is the first rpc endpoint url.
200200
// TODO: Update toMultichainNetworkConfigurationsByChainId to handle this case.
201-
const evmNetworks = Object.entries(networkConfigurationsByChainId).reduce(
202-
(acc, [, network]) => ({
203-
...acc,
204-
[toEvmCaipChainId(network.chainId)]: {
205-
...toMultichainNetworkConfiguration(network),
206-
name:
207-
network.name ||
208-
network.rpcEndpoints[network.defaultRpcEndpointIndex].url,
209-
},
210-
}),
211-
{},
212-
);
201+
const evmNetworks: Record<
202+
CaipChainId,
203+
InternalMultichainNetworkConfiguration
204+
> = {};
205+
206+
for (const [, network] of Object.entries(
207+
networkConfigurationsByChainId,
208+
)) {
209+
evmNetworks[toEvmCaipChainId(network.chainId)] = {
210+
...toMultichainNetworkConfiguration(network),
211+
name:
212+
network.name ||
213+
network.rpcEndpoints[network.defaultRpcEndpointIndex].url,
214+
};
215+
}
213216

214217
const networks = {
215218
///: BEGIN:ONLY_INCLUDE_IF(multichain)

0 commit comments

Comments
 (0)