Skip to content

Commit

Permalink
chore: replace proxy-memoize with reselect (#15333)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nodonisko authored Nov 11, 2024
1 parent acf7518 commit a33bf1d
Show file tree
Hide file tree
Showing 70 changed files with 1,459 additions and 1,393 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@
"@types/node": "20.12.7",
"@types/react": "18.2.55",
"bn.js": "5.2.1",
"node-gyp": "10.2.0"
"node-gyp": "10.2.0",
"reselect": "5.1.1"
},
"devDependencies": {
"@babel/cli": "^7.23.9",
Expand Down
1 change: 0 additions & 1 deletion packages/suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@
"pako": "^2.1.0",
"pdfmake": "^0.2.9",
"polished": "^4.3.1",
"proxy-memoize": "2.0.2",
"qrcode.react": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TransactionsRootState,
StakeRootState,
selectPoolStatsApyData,
AccountsRootState,
} from '@suite-common/wallet-core';

import { Translation } from 'src/components/suite';
Expand All @@ -25,7 +26,7 @@ export const StakingInfo = ({ isExpanded }: StakingInfoProps) => {
const { data } =
useSelector((state: StakeRootState) => selectValidatorsQueue(state, account?.symbol)) || {};

const stakeTxs = useSelector((state: TransactionsRootState) =>
const stakeTxs = useSelector((state: TransactionsRootState & AccountsRootState) =>
selectAccountStakeTransactions(state, account?.key ?? ''),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
selectValidatorsQueue,
TransactionsRootState,
StakeRootState,
AccountsRootState,
} from '@suite-common/wallet-core';

import { Translation } from 'src/components/suite';
Expand All @@ -24,7 +25,7 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => {
const { data } =
useSelector((state: StakeRootState) => selectValidatorsQueue(state, account?.symbol)) || {};

const unstakeTxs = useSelector((state: TransactionsRootState) =>
const unstakeTxs = useSelector((state: TransactionsRootState & AccountsRootState) =>
selectAccountUnstakeTransactions(state, account?.key ?? ''),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const AccountSection = ({
} = account;

const coinDefinitions = useSelector(state => selectCoinDefinitions(state, symbol));
const hasStaked = useSelector(state => selectAccountHasStaked(state, account));
const hasStaked = useSelector(state => selectAccountHasStaked(state, account.key));

const isStakeShown = isSupportedEthStakingNetworkSymbol(symbol) && hasStaked;

Expand Down
10 changes: 6 additions & 4 deletions packages/suite/src/reducers/wallet/coinjoinReducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import produce from 'immer';
import { memoizeWithArgs } from 'proxy-memoize';

import { BigNumber } from '@trezor/utils/src/bigNumber';
import { getInputSize, getOutputSize, RoundPhase } from '@trezor/coinjoin';
Expand All @@ -18,6 +17,7 @@ import {
selectIsFeatureDisabled,
selectFeatureConfig,
} from '@suite-common/message-system';
import { createWeakMapSelector } from '@suite-common/redux-utils';

import { STORAGE } from 'src/actions/suite/constants';
import {
Expand Down Expand Up @@ -610,6 +610,8 @@ export const coinjoinReducer = (
}
});

const createMemoizedSelector = createWeakMapSelector.withTypes<CoinjoinRootState>();

export const selectCoinjoinAccounts = (state: CoinjoinRootState) => state.wallet.coinjoin.accounts;

export const selectCoinjoinClients = (state: CoinjoinRootState) => state.wallet.coinjoin.clients;
Expand Down Expand Up @@ -672,9 +674,9 @@ export const selectCurrentCoinjoinBalanceBreakdown = (state: CoinjoinRootState)
return balanceBreakdown;
};

export const selectRegisteredUtxosByAccountKey = memoizeWithArgs(
(state: CoinjoinRootState, accountKey: AccountKey) => {
const coinjoinAccount = selectCoinjoinAccountByKey(state, accountKey);
export const selectRegisteredUtxosByAccountKey = createMemoizedSelector(
[selectCoinjoinAccountByKey],
coinjoinAccount => {
if (!coinjoinAccount?.prison) return;
const { prison, session, transactionCandidates } = coinjoinAccount;

Expand Down
4 changes: 2 additions & 2 deletions packages/suite/src/views/settings/SettingsDebug/Backends.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ const BackendItem = ({
};

const CoinItem = ({ symbol }: { symbol: NetworkSymbol }) => {
const { url, error, connected, reconnectionTime, identityConnections } = useSelector(
selectNetworkBlockchainInfo(symbol),
const { url, error, connected, reconnectionTime, identityConnections } = useSelector(state =>
selectNetworkBlockchainInfo(state, symbol),
);

const dispatch = useDispatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const BitcoinOptions = () => {
setDraftSaveRequest(true);
};

const blockchain = useSelector(selectNetworkBlockchainInfo(network.symbol));
const blockchain = useSelector(state => selectNetworkBlockchainInfo(state, network.symbol));

const locktime = watch('bitcoinLockTime');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const Locktime = ({ close }: LocktimeProps) => {
watch,
} = useSendFormContext();

const blockchain = useSelector(selectNetworkBlockchainInfo(network.symbol));
const blockchain = useSelector(state => selectNetworkBlockchainInfo(state, network.symbol));

const { translationString } = useTranslation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ interface EthStakingDashboardProps {
}

export const EthStakingDashboard = ({ selectedAccount }: EthStakingDashboardProps) => {
const hasStaked = useSelector(state => selectAccountHasStaked(state, selectedAccount.account));
const hasStaked = useSelector(state =>
selectAccountHasStaked(state, selectedAccount.account.key),
);

return (
<WalletLayout title="TR_STAKE_ETH" account={selectedAccount}>
Expand Down
2 changes: 1 addition & 1 deletion scripts/list-outdated-dependencies/common-dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ nx
octokit
patch-package
prettier
proxy-memoize
raw-loader
react
react-dom
Expand All @@ -88,6 +87,7 @@ redux-logger
redux-mock-store
redux-thunk
rimraf
reselect
sort-package-json
tar
ts-mixer
Expand Down
1 change: 0 additions & 1 deletion suite-common/message-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"fs-extra": "^11.2.0",
"json-schema-to-typescript": "^13.1.2",
"jws": "^4.0.0",
"proxy-memoize": "2.0.2",
"semver": "^7.6.3"
},
"devDependencies": {
Expand Down
145 changes: 79 additions & 66 deletions suite-common/message-system/src/messageSystemSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { memoize, memoizeWithArgs } from 'proxy-memoize';

import { createWeakMapSelector, returnStableArrayIfEmpty } from '@suite-common/redux-utils';
import { Message, Category } from '@suite-common/suite-types';

import { ContextDomain, FeatureDomain, MessageSystemRootState } from './messageSystemTypes';

// Create app-specific selectors with correct types
const createMemoizedSelector = createWeakMapSelector.withTypes<MessageSystemRootState>();

// Basic selectors don't need memoization
export const selectMessageSystemConfig = (state: MessageSystemRootState) =>
state.messageSystem.config;

Expand All @@ -16,53 +19,57 @@ export const selectMessageSystemCurrentSequence = (state: MessageSystemRootState
const comparePriority = (a: Message, b: Message) => b.priority - a.priority;

const makeSelectActiveMessagesByCategory = (category: Category) =>
memoize((state: MessageSystemRootState) => {
const { config, validMessages, dismissedMessages } = state.messageSystem;
const nonDismissedValidMessages = validMessages[category].filter(
id => !dismissedMessages[id]?.[category],
);

const messages = config?.actions
.filter(({ message }) => nonDismissedValidMessages.includes(message.id))
.map(action => action.message);

if (!messages?.length) return [];

return messages.sort(comparePriority);
});
createMemoizedSelector(
[
state => state.messageSystem.config,
state => state.messageSystem.validMessages[category],
state => state.messageSystem.dismissedMessages,
],
(config, validMessages, dismissedMessages) => {
const nonDismissedValidMessages = validMessages.filter(
id => !dismissedMessages[id]?.[category],
);

const messages = config?.actions
.filter(({ message }) => nonDismissedValidMessages.includes(message.id))
.map(action => action.message);

return returnStableArrayIfEmpty(messages?.sort(comparePriority));
},
);

export const selectActiveBannerMessages = makeSelectActiveMessagesByCategory('banner');
export const selectActiveContextMessages = makeSelectActiveMessagesByCategory('context');
export const selectActiveModalMessages = makeSelectActiveMessagesByCategory('modal');
export const selectActiveFeatureMessages = makeSelectActiveMessagesByCategory('feature');

export const selectIsAnyBannerMessageActive = (state: MessageSystemRootState) => {
const activeBannerMessages = selectActiveBannerMessages(state);

return activeBannerMessages.length > 0;
};

export const selectBannerMessage = memoize((state: MessageSystemRootState) => {
const activeBannerMessages = selectActiveBannerMessages(state);

return activeBannerMessages[0];
});
export const selectIsAnyBannerMessageActive = createMemoizedSelector(
[selectActiveBannerMessages],
activeBannerMessages => activeBannerMessages.length > 0,
);

export const selectContextMessage = memoizeWithArgs(
(state: MessageSystemRootState, domain: ContextDomain) => {
const activeContextMessages = selectActiveContextMessages(state);
export const selectBannerMessage = createMemoizedSelector(
[selectActiveBannerMessages],
activeBannerMessages => activeBannerMessages[0],
);

return activeContextMessages.find(message => message.context?.domain === domain);
},
export const selectContextMessage = createMemoizedSelector(
[selectActiveContextMessages, (_state, domain: ContextDomain) => domain],
(activeContextMessages, domain) =>
activeContextMessages.find(message => message.context?.domain === domain),
);

export const selectContextMessageContent = memoizeWithArgs(
(state: MessageSystemRootState, domain: ContextDomain, language: string) => {
const activeContextMessages = selectActiveContextMessages(state);
export const selectContextMessageContent = createMemoizedSelector(
[
selectActiveContextMessages,
(_state, domain: ContextDomain) => domain,
(_state, _domain, language: string) => language,
],
(activeContextMessages, domain, language) => {
const message = activeContextMessages.find(
activeContextMessage => activeContextMessage.context?.domain === domain,
);
if (!message) return;
if (!message) return undefined;

return {
...message,
Expand All @@ -77,32 +84,31 @@ export const selectContextMessageContent = memoizeWithArgs(
},
);

export const selectFeatureMessage = memoizeWithArgs(
(state: MessageSystemRootState, domain: FeatureDomain) => {
const activeFeatureMessages = selectActiveFeatureMessages(state);

return activeFeatureMessages.find(message =>
export const selectFeatureMessage = createMemoizedSelector(
[selectActiveFeatureMessages, (_state, domain: FeatureDomain) => domain],
(activeFeatureMessages, domain) =>
activeFeatureMessages.find(message =>
message.feature?.some(feature => feature.domain === domain),
);
},
),
);

export const selectFeatureMessageContent = memoizeWithArgs(
(state: MessageSystemRootState, domain: FeatureDomain, language: string) => {
const featureMessages = selectFeatureMessage(state, domain);

return featureMessages?.content[language] ?? featureMessages?.content.en;
},
export const selectFeatureMessageContent = createMemoizedSelector(
[
selectFeatureMessage,
(_state, domain: FeatureDomain) => domain,
(_state, _domain, language: string) => language,
],
(featureMessages, _domain, language) =>
featureMessages?.content[language] ?? featureMessages?.content.en,
);

export const selectFeatureConfig = memoizeWithArgs(
(state: MessageSystemRootState, domain: FeatureDomain) => {
const featureMessages = selectFeatureMessage(state, domain);

return featureMessages?.feature?.find(feature => feature.domain === domain);
},
export const selectFeatureConfig = createMemoizedSelector(
[selectFeatureMessage, (_state, domain: FeatureDomain) => domain],
(featureMessages, domain) =>
featureMessages?.feature?.find(feature => feature.domain === domain),
);

// These don't need memoization as they're simple computations
export const selectIsFeatureEnabled = (
state: MessageSystemRootState,
domain: FeatureDomain,
Expand All @@ -123,14 +129,21 @@ export const selectIsFeatureDisabled = (
return typeof featureFlag === 'boolean' ? !featureFlag : defaultValue ?? false;
};

export const selectAllValidMessages = memoize((state: MessageSystemRootState) => {
const { validMessages, config } = state.messageSystem;
const allValidMessages = [
...validMessages.banner,
...validMessages.feature,
...validMessages.modal,
...validMessages.context,
];

return config?.actions.map(a => a.message).filter(m => allValidMessages.includes(m.id)) || [];
});
const selectValidMessages = (state: MessageSystemRootState) => state.messageSystem.validMessages;
const selectConfig = (state: MessageSystemRootState) => state.messageSystem.config;

export const selectAllValidMessages = createMemoizedSelector(
[selectValidMessages, selectConfig],
(validMessages, config) => {
const allValidMessages = [
...validMessages.banner,
...validMessages.feature,
...validMessages.modal,
...validMessages.context,
];

return returnStableArrayIfEmpty(
config?.actions.map(a => a.message).filter(m => allValidMessages.includes(m.id)),
);
},
);
3 changes: 2 additions & 1 deletion suite-common/redux-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"lodash": "^4.17.21",
"react": "18.2.0",
"react-redux": "8.0.7",
"redux": "^4.2.1"
"redux": "^4.2.1",
"reselect": "5.1.1"
}
}
1 change: 1 addition & 0 deletions suite-common/redux-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './createReducerWithExtraDeps';
export * from './createActionWithExtraDeps';
export * from './createSingleInstanceThunk';
export * from './hooks/useSelectorDeepComparison';
export * from './selectorsUtils';
17 changes: 17 additions & 0 deletions suite-common/redux-utils/src/selectorsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createSelectorCreator, weakMapMemoize } from 'reselect';

const EMPTY_STABLE_ARRAY: unknown[] = [];

/**
* Returns a stable empty array reference instead of creating a new empty array each time.
* This helps prevent unnecessary re-renders when using empty arrays in React components.
*/
export const returnStableArrayIfEmpty = <T>(array?: readonly T[] | T[]): T[] => {
return array && array.length > 0 ? (array as T[]) : (EMPTY_STABLE_ARRAY as T[]);
};

// For selectors with parameters, use WeakMap memoization
export const createWeakMapSelector = createSelectorCreator({
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize,
});
3 changes: 1 addition & 2 deletions suite-common/toast-notifications/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"@suite-common/suite-types": "workspace:*",
"@suite-common/test-utils": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
"@trezor/connect": "workspace:*",
"proxy-memoize": "2.0.2"
"@trezor/connect": "workspace:*"
}
}
Loading

0 comments on commit a33bf1d

Please sign in to comment.