From e309adcd33ccebdc4777694e4ea665dd59f1be58 Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Sun, 10 May 2026 23:33:43 -0600 Subject: [PATCH 1/3] Optimize settings and nodes selectors --- src-gui/src/store/hooks.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 910bad3a2d..51a8735f80 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -18,7 +18,12 @@ import { isPendingPasswordApprovalEvent, isContextFullyInitialized, } from "models/tauriModelExt"; -import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import { + shallowEqual, + TypedUseSelectorHook, + useDispatch, + useSelector, +} from "react-redux"; import type { AppDispatch, RootState } from "renderer/store/storeRenderer"; import { parseDateString } from "utils/parseUtils"; import { useEffect, useMemo, useState } from "react"; @@ -191,13 +196,11 @@ export function useAreSwapInfosLoaded(): boolean { } export function useSettings(selector: (settings: SettingsState) => T): T { - const settings = useAppSelector((state) => state.settings); - return selector(settings); + return useAppSelector((state) => selector(state.settings), shallowEqual); } export function useNodes(selector: (nodes: NodesSlice) => T): T { - const nodes = useAppSelector((state) => state.nodes); - return selector(nodes); + return useAppSelector((state) => selector(state.nodes), shallowEqual); } export function usePendingApprovals(): PendingApprovalRequest[] { From 18bb1571d39187499bac5afe2158835d24a57f2b Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Sun, 10 May 2026 23:41:07 -0600 Subject: [PATCH 2/3] Memoize shared UI selector derivations --- src-gui/src/store/hooks.ts | 69 ++++++++++---------------------------- 1 file changed, 17 insertions(+), 52 deletions(-) diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 51a8735f80..afca1b904f 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -1,21 +1,14 @@ -import { sortBy, sum, throttle } from "lodash"; +import { sortBy, throttle } from "lodash"; import { BobStateName, GetSwapInfoResponseExt, - isBitcoinSyncProgress, - isPendingBackgroundProcess, - isPendingLockBitcoinApprovalEvent, - isPendingSeedSelectionApprovalEvent, PendingApprovalRequest, PendingLockBitcoinApprovalRequest, PendingSelectMakerApprovalRequest, - isPendingSelectMakerApprovalEvent, haveFundsBeenLocked, PendingSeedSelectionApprovalRequest, PendingSendMoneroApprovalRequest, - isPendingSendMoneroApprovalEvent, PendingPasswordApprovalRequest, - isPendingPasswordApprovalEvent, isContextFullyInitialized, } from "models/tauriModelExt"; import { @@ -40,7 +33,15 @@ import { Alert } from "models/apiModel"; import { fnv1a } from "utils/hash"; import { selectAllSwapInfos, + selectBitcoinSyncProgress, + selectConservativeBitcoinSyncProgress, + selectPendingBackgroundProcesses, selectPendingApprovals, + selectPendingLockBitcoinApprovals, + selectPendingPasswordApprovals, + selectPendingSeedSelectionApprovals, + selectPendingSelectMakerApprovals, + selectPendingSendMoneroApprovals, selectSwapInfoWithTimelock, selectSwapInfosRaw, } from "./selectors"; @@ -208,8 +209,7 @@ export function usePendingApprovals(): PendingApprovalRequest[] { } export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] { - const approvals = usePendingApprovals(); - return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c)); + return useAppSelector(selectPendingLockBitcoinApprovals); } export function useMoneroMainAddress(): string | null { @@ -221,23 +221,19 @@ export function useMoneroSubaddresses(): SubaddressSummary[] { } export function usePendingSendMoneroApproval(): PendingSendMoneroApprovalRequest[] { - const approvals = usePendingApprovals(); - return approvals.filter((c) => isPendingSendMoneroApprovalEvent(c)); + return useAppSelector(selectPendingSendMoneroApprovals); } export function usePendingSelectMakerApproval(): PendingSelectMakerApprovalRequest[] { - const approvals = usePendingApprovals(); - return approvals.filter((c) => isPendingSelectMakerApprovalEvent(c)); + return useAppSelector(selectPendingSelectMakerApprovals); } export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] { - const approvals = usePendingApprovals(); - return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c)); + return useAppSelector(selectPendingSeedSelectionApprovals); } export function usePendingPasswordApproval(): PendingPasswordApprovalRequest[] { - const approvals = usePendingApprovals(); - return approvals.filter((c) => isPendingPasswordApprovalEvent(c)); + return useAppSelector(selectPendingPasswordApprovals); } /// Returns all the pending background processes @@ -246,22 +242,11 @@ export function usePendingBackgroundProcesses(): [ string, TauriBackgroundProgress, ][] { - const background = useAppSelector((state) => state.rpc.state.background); - return Object.entries(background).filter(([_, c]) => - isPendingBackgroundProcess(c), - ); + return useAppSelector(selectPendingBackgroundProcesses); } export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] { - const pendingProcesses = usePendingBackgroundProcesses(); - const syncingProcesses = pendingProcesses - .map(([_, c]) => c) - .filter(isBitcoinSyncProgress); - return syncingProcesses - .map((c) => c.progress.content) - .filter( - (content): content is TauriBitcoinSyncProgress => content !== undefined, - ); + return useAppSelector(selectBitcoinSyncProgress); } export function useIsSyncingBitcoin(): boolean { @@ -274,27 +259,7 @@ export function useIsSyncingBitcoin(): boolean { /// If at least one sync is known, it returns {type: "Known", content: {consumed, total}} /// where consumed and total are the sum of all the consumed and total values of the syncs export function useConservativeBitcoinSyncProgress(): TauriBitcoinSyncProgress | null { - const syncingProcesses = useBitcoinSyncProgress(); - const progressValues = syncingProcesses.map((c) => c.content?.consumed ?? 0); - const totalValues = syncingProcesses.map((c) => c.content?.total ?? 0); - - const progress = sum(progressValues); - const total = sum(totalValues); - - // If either the progress or the total is 0, we consider the sync to be unknown - if (progress === 0 || total === 0) { - return { - type: "Unknown", - }; - } - - return { - type: "Known", - content: { - consumed: progress, - total: total, - }, - }; + return useAppSelector(selectConservativeBitcoinSyncProgress); } /** From 91d0c59ba0f33a9d30811b04b2559a04bd7968a7 Mon Sep 17 00:00:00 2001 From: KingParmenides Date: Sun, 10 May 2026 23:41:14 -0600 Subject: [PATCH 3/3] Memoize shared UI selector derivations --- src-gui/src/store/selectors.ts | 86 +++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src-gui/src/store/selectors.ts b/src-gui/src/store/selectors.ts index 0a59efed08..dcbf30cbed 100644 --- a/src-gui/src/store/selectors.ts +++ b/src-gui/src/store/selectors.ts @@ -1,10 +1,20 @@ import { createSelector } from "@reduxjs/toolkit"; import { RootState } from "renderer/store/storeRenderer"; -import { GetSwapInfoResponseExt } from "models/tauriModelExt"; +import { + GetSwapInfoResponseExt, + isBitcoinSyncProgress, + isPendingBackgroundProcess, + isPendingLockBitcoinApprovalEvent, + isPendingPasswordApprovalEvent, + isPendingSeedSelectionApprovalEvent, + isPendingSelectMakerApprovalEvent, + isPendingSendMoneroApprovalEvent, +} from "models/tauriModelExt"; import { ConnectionStatus, ExpiredTimelocks, QuoteStatus, + TauriBitcoinSyncProgress, } from "models/tauriModel"; const selectRpcState = (state: RootState) => state.rpc.state; @@ -60,6 +70,80 @@ export const selectPendingApprovals = createSelector( ), ); +export const selectPendingLockBitcoinApprovals = createSelector( + [selectPendingApprovals], + (approvals) => approvals.filter(isPendingLockBitcoinApprovalEvent), +); + +export const selectPendingSendMoneroApprovals = createSelector( + [selectPendingApprovals], + (approvals) => approvals.filter(isPendingSendMoneroApprovalEvent), +); + +export const selectPendingSelectMakerApprovals = createSelector( + [selectPendingApprovals], + (approvals) => approvals.filter(isPendingSelectMakerApprovalEvent), +); + +export const selectPendingSeedSelectionApprovals = createSelector( + [selectPendingApprovals], + (approvals) => approvals.filter(isPendingSeedSelectionApprovalEvent), +); + +export const selectPendingPasswordApprovals = createSelector( + [selectPendingApprovals], + (approvals) => approvals.filter(isPendingPasswordApprovalEvent), +); + +export const selectPendingBackgroundProcesses = createSelector( + [selectRpcState], + (rpcState) => + Object.entries(rpcState.background).filter(([, progress]) => + isPendingBackgroundProcess(progress), + ), +); + +export const selectBitcoinSyncProgress = createSelector( + [selectPendingBackgroundProcesses], + (pendingProcesses): TauriBitcoinSyncProgress[] => + pendingProcesses + .map(([, progress]) => progress) + .filter(isBitcoinSyncProgress) + .map((progress) => progress.progress.content) + .filter( + (content): content is TauriBitcoinSyncProgress => + content !== undefined, + ), +); + +export const selectConservativeBitcoinSyncProgress = createSelector( + [selectBitcoinSyncProgress], + (syncingProcesses): TauriBitcoinSyncProgress | null => { + const progress = syncingProcesses.reduce( + (total, current) => total + (current.content?.consumed ?? 0), + 0, + ); + const total = syncingProcesses.reduce( + (sum, current) => sum + (current.content?.total ?? 0), + 0, + ); + + if (progress === 0 || total === 0) { + return { + type: "Unknown", + }; + } + + return { + type: "Known", + content: { + consumed: progress, + total, + }, + }; + }, +); + // TODO: This should be split into multiple selectors/hooks to avoid excessive re-rendering export const selectPeers = createSelector([selectP2pState], (p2p) => { const peerIds = new Set([