From 2b86306f1c189b31cd941951625b3930bc1096b6 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 15 May 2026 11:14:34 -0300 Subject: [PATCH 1/3] fix(web3): use useAccountEffect to avoid wagmi tracked re-renders #2974 destructured `status` and `connector` from `useAccount()`. Wagmi v2's useAccount uses useSyncExternalStoreWithTracked, which adds each destructured property to a tracked-key set and deep-equals those keys on every store tick. Tracking the connector object forced a per-tick deep-equal of a fat object, re-rendering Web3ContextProvider and everything below it on every wagmi internal update. Tab switching went from ~24ms p50 to ~66ms p50. useAccountEffect calls watchAccount inside a plain useEffect, so the component is not subscribed to renders. It fires for fresh connect and reconnect, so the original analytics fix still holds. --- src/libs/web3-data-provider/Web3Provider.tsx | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/libs/web3-data-provider/Web3Provider.tsx b/src/libs/web3-data-provider/Web3Provider.tsx index a0f0da5484..1e72937f93 100644 --- a/src/libs/web3-data-provider/Web3Provider.tsx +++ b/src/libs/web3-data-provider/Web3Provider.tsx @@ -8,7 +8,7 @@ import { useRootStore } from 'src/store/root'; import { wagmiConfig } from 'src/ui-config/wagmiConfig'; import { hexToAscii } from 'src/utils/utils'; import { UserRejectedRequestError } from 'viem'; -import { useAccount, useConnect, useSwitchChain, useWatchAsset } from 'wagmi'; +import { useAccount, useAccountEffect, useConnect, useSwitchChain, useWatchAsset } from 'wagmi'; import { useShallow } from 'zustand/shallow'; import { Web3Context } from '../hooks/useWeb3Context'; @@ -44,7 +44,7 @@ let didAutoConnectForCypress = false; export const Web3ContextProvider: React.FC<{ children: ReactElement }> = ({ children }) => { const { switchChainAsync } = useSwitchChain(); const { watchAssetAsync } = useWatchAsset(); - const { chainId, address, status, connector } = useAccount(); + const { chainId, address } = useAccount(); const { connect, connectors } = useConnect(); const [readOnlyModeAddress, setReadOnlyModeAddress] = useState(); @@ -207,16 +207,20 @@ export const Web3ContextProvider: React.FC<{ children: ReactElement }> = ({ chil setAccount(account?.toLowerCase()); }, [account, setAccount]); - // Drive walletType from wagmi's account state so it survives page reloads. - // ConnectKit's `onConnect` prop only fires on fresh connect (not on reconnect), - // which caused most transaction events to be tracked with walletType=undefined. - useEffect(() => { - if (status === 'connected' && connector?.id) { + // Drive walletType from wagmi's account events. useAccountEffect fires for both + // fresh connects and reconnects (page reload, redirect-back), so analytics keeps + // a real connector id instead of falling back to undefined after a refresh. + // It uses watchAccount under the hood and does not subscribe this component to + // account state, so destructuring extra fields here would force wagmi's tracked + // subscription to deep-equal the connector object on every store tick. + useAccountEffect({ + onConnect({ connector }) { setWalletType(connector.id); - } else if (status === 'disconnected') { + }, + onDisconnect() { setWalletType(undefined); - } - }, [status, connector?.id, setWalletType]); + }, + }); useEffect(() => { if (readOnlyModeAddress) { From 70552e269355bec626f4ce2a85515fbd26efef4f Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 15 May 2026 11:18:10 -0300 Subject: [PATCH 2/3] style(web3): shorten useAccountEffect comment --- src/libs/web3-data-provider/Web3Provider.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/web3-data-provider/Web3Provider.tsx b/src/libs/web3-data-provider/Web3Provider.tsx index 1e72937f93..590c4d604e 100644 --- a/src/libs/web3-data-provider/Web3Provider.tsx +++ b/src/libs/web3-data-provider/Web3Provider.tsx @@ -207,12 +207,8 @@ export const Web3ContextProvider: React.FC<{ children: ReactElement }> = ({ chil setAccount(account?.toLowerCase()); }, [account, setAccount]); - // Drive walletType from wagmi's account events. useAccountEffect fires for both - // fresh connects and reconnects (page reload, redirect-back), so analytics keeps - // a real connector id instead of falling back to undefined after a refresh. - // It uses watchAccount under the hood and does not subscribe this component to - // account state, so destructuring extra fields here would force wagmi's tracked - // subscription to deep-equal the connector object on every store tick. + // useAccountEffect fires on connect AND reconnect (so analytics survives reloads) + // without subscribing this component to wagmi's tracked re-renders. useAccountEffect({ onConnect({ connector }) { setWalletType(connector.id); From 204cedf192f49a49531d02e925cf123e382590ad Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Fri, 15 May 2026 16:06:14 -0300 Subject: [PATCH 3/3] fix(web3): use watchAccount to cover reconnect path missed by useAccountEffect useAccountEffect.onConnect skips the connecting(address) -> connected transition (wagmi#4221), so walletType regresses to undefined when a user reloads with an extension permission flow. Subscribe directly to the wagmi store via watchAccount, which fires on every transition and keeps this component out of React's render path. Seed via getAccount to cover stores already connected before the effect installs. --- src/libs/web3-data-provider/Web3Provider.tsx | 32 ++++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/libs/web3-data-provider/Web3Provider.tsx b/src/libs/web3-data-provider/Web3Provider.tsx index 590c4d604e..951863e2ce 100644 --- a/src/libs/web3-data-provider/Web3Provider.tsx +++ b/src/libs/web3-data-provider/Web3Provider.tsx @@ -1,6 +1,7 @@ import { API_ETH_MOCK_ADDRESS, ERC20Service, transactionType } from '@aave/contract-helpers'; import { SignatureLike } from '@ethersproject/bytes'; import { JsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; +import { getAccount, watchAccount } from '@wagmi/core'; import { BigNumber, PopulatedTransaction, utils } from 'ethers'; import React, { ReactElement, useEffect, useState } from 'react'; import { useIsContractAddress } from 'src/hooks/useIsContractAddress'; @@ -8,7 +9,7 @@ import { useRootStore } from 'src/store/root'; import { wagmiConfig } from 'src/ui-config/wagmiConfig'; import { hexToAscii } from 'src/utils/utils'; import { UserRejectedRequestError } from 'viem'; -import { useAccount, useAccountEffect, useConnect, useSwitchChain, useWatchAsset } from 'wagmi'; +import { useAccount, useConnect, useSwitchChain, useWatchAsset } from 'wagmi'; import { useShallow } from 'zustand/shallow'; import { Web3Context } from '../hooks/useWeb3Context'; @@ -207,16 +208,27 @@ export const Web3ContextProvider: React.FC<{ children: ReactElement }> = ({ chil setAccount(account?.toLowerCase()); }, [account, setAccount]); - // useAccountEffect fires on connect AND reconnect (so analytics survives reloads) - // without subscribing this component to wagmi's tracked re-renders. - useAccountEffect({ - onConnect({ connector }) { + // Drive walletType from a direct wagmi store subscription. watchAccount fires for + // every state transition into `connected` (including the `connecting(address) -> + // connected` path that useAccountEffect.onConnect misses, see wagmi#4221), and + // it does not subscribe this component to React renders. Seed once on mount via + // getAccount in case the store is already `connected` before the effect installs. + useEffect(() => { + const { status, connector } = getAccount(wagmiConfig); + if (status === 'connected' && connector?.id) { setWalletType(connector.id); - }, - onDisconnect() { - setWalletType(undefined); - }, - }); + } + + return watchAccount(wagmiConfig, { + onChange(data) { + if (data.status === 'connected' && data.connector?.id) { + setWalletType(data.connector.id); + } else if (data.status === 'disconnected') { + setWalletType(undefined); + } + }, + }); + }, [setWalletType]); useEffect(() => { if (readOnlyModeAddress) {