Skip to content

Commit ece07a7

Browse files
refactor: centralize media cache busting in TransactionNotifications
Move bustMediaCache calls from individual transaction files to TransactionNotifications callback. This centralizes cache invalidation logic and runs it after transaction confirmation rather than during transaction preparation. Use ref pattern for getLatestTransaction to prevent callback recreation when state.items changes, avoiding cascading re-renders that could restart gas estimation in TransactionStageModal. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 38b98cd commit ece07a7

File tree

8 files changed

+104
-74
lines changed

8 files changed

+104
-74
lines changed

src/components/TransactionNotifications.tsx

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { useQueryClient } from '@tanstack/react-query'
2-
import { useCallback, useEffect, useState } from 'react'
2+
import { useCallback, useEffect, useRef, useState } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import styled, { css } from 'styled-components'
5+
import { useClient } from 'wagmi'
56

67
import { Button, Toast } from '@ensdomains/thorin'
78

89
import { useChainName } from '@app/hooks/chain/useChainName'
910
import { META_DATA_QUERY_KEY } from '@app/hooks/useEnsAvatar'
1011
import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider'
12+
import { ClientWithEns } from '@app/types'
1113
import { useBreakpoint } from '@app/utils/BreakpointProvider'
14+
import { bustMediaCache } from '@app/utils/metadataCache'
1215
import { UpdateCallback, useCallbackOnTransaction } from '@app/utils/SyncProvider/SyncProvider'
1316
import { makeEtherscanLink } from '@app/utils/utils'
1417

@@ -36,40 +39,127 @@ export const TransactionNotifications = () => {
3639

3740
const chainName = useChainName()
3841
const queryClient = useQueryClient()
42+
const client = useClient()
3943

4044
const [open, setOpen] = useState(false)
4145

42-
const { resumeTransactionFlow, getResumable } = useTransactionFlow()
46+
const { resumeTransactionFlow, getResumable, getLatestTransaction } = useTransactionFlow()
4347

4448
const [notificationQueue, setNotificationQueue] = useState<Notification[]>([])
4549
const currentNotification = notificationQueue[0]
4650

51+
// Use ref to avoid recreating callback when state.items changes
52+
// This prevents cascading re-renders that can restart gas estimation
53+
const getLatestTransactionRef = useRef(getLatestTransaction)
54+
useEffect(() => {
55+
getLatestTransactionRef.current = getLatestTransaction
56+
}, [getLatestTransaction])
57+
4758
const updateCallback = useCallback<UpdateCallback>(
4859
({ action, key, status, hash }) => {
4960
if (status === 'pending' || status === 'repriced') return
5061
if (status === 'confirmed') {
62+
// Get transaction data for cache busting (use ref to avoid dependency)
63+
const latestTx = key ? getLatestTransactionRef.current(key) : undefined
64+
const txData = latestTx?.data as { name?: string; records?: unknown } | undefined
65+
const ensName = txData?.name
66+
5167
switch (action) {
52-
case 'registerName':
68+
case 'registerName': {
5369
trackEvent('register', chainName)
70+
// Bust cache for avatar/header if being set during registration
71+
if (ensName && client) {
72+
const registrationData = txData as {
73+
name: string
74+
records?: { texts?: Array<{ key: string }> }
75+
}
76+
const hasAvatarChange = registrationData.records?.texts?.some(
77+
(t) => t.key === 'avatar',
78+
)
79+
const hasHeaderChange = registrationData.records?.texts?.some(
80+
(t) => t.key === 'header',
81+
)
82+
if (hasAvatarChange)
83+
bustMediaCache(ensName, client as ClientWithEns, 'avatar')
84+
if (hasHeaderChange)
85+
bustMediaCache(ensName, client as ClientWithEns, 'header')
86+
}
5487
queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] })
5588
break
89+
}
5690
case 'commitName':
5791
trackEvent('commit', chainName)
5892
break
5993
case 'extendNames':
6094
trackEvent('renew', chainName)
6195
break
62-
case 'updateProfileRecords':
96+
case 'updateResolver':
97+
case 'resetProfile': {
98+
// These actions always bust both avatar and header
99+
if (ensName && client) {
100+
bustMediaCache(ensName, client as ClientWithEns)
101+
}
102+
queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] })
103+
break
104+
}
63105
case 'updateProfile':
64-
case 'resetProfile':
106+
case 'updateProfileRecords': {
107+
// Check if avatar or header are being modified in records
108+
if (ensName && client) {
109+
const profileData = txData as {
110+
name: string
111+
records?:
112+
| { texts?: Array<{ key: string }> }
113+
| Array<{ key: string; group?: string }>
114+
}
115+
// Handle both RecordOptions format and ProfileRecord[] format
116+
const records = profileData.records
117+
let hasAvatarChange = false
118+
let hasHeaderChange = false
119+
if (Array.isArray(records)) {
120+
// ProfileRecord[] format (updateProfileRecords)
121+
hasAvatarChange = records.some(
122+
(r) => r.key === 'avatar' && (r as { group?: string }).group === 'media',
123+
)
124+
hasHeaderChange = records.some(
125+
(r) => r.key === 'header' && (r as { group?: string }).group === 'media',
126+
)
127+
} else if (records?.texts) {
128+
// RecordOptions format (updateProfile)
129+
hasAvatarChange = records.texts.some((t) => t.key === 'avatar')
130+
hasHeaderChange = records.texts.some((t) => t.key === 'header')
131+
}
132+
if (hasAvatarChange)
133+
bustMediaCache(ensName, client as ClientWithEns, 'avatar')
134+
if (hasHeaderChange)
135+
bustMediaCache(ensName, client as ClientWithEns, 'header')
136+
}
137+
queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] })
138+
break
139+
}
65140
case 'resetProfileWithRecords':
66141
case 'migrateProfile':
67-
case 'migrateProfileWithReset':
68-
case 'updateResolver':
69-
// Cache busting now handled in transaction files
70-
// Still need to invalidate queries to trigger refetch
142+
case 'migrateProfileWithReset': {
143+
// Check if avatar or header are in the records being set
144+
if (ensName && client) {
145+
const recordsData = txData as {
146+
name: string
147+
records?: { texts?: Array<{ key: string }> }
148+
}
149+
const hasAvatarChange = recordsData.records?.texts?.some(
150+
(t) => t.key === 'avatar',
151+
)
152+
const hasHeaderChange = recordsData.records?.texts?.some(
153+
(t) => t.key === 'header',
154+
)
155+
if (hasAvatarChange)
156+
bustMediaCache(ensName, client as ClientWithEns, 'avatar')
157+
if (hasHeaderChange)
158+
bustMediaCache(ensName, client as ClientWithEns, 'header')
159+
}
71160
queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] })
72161
break
162+
}
73163
default:
74164
break
75165
}
@@ -106,7 +196,7 @@ export const TransactionNotifications = () => {
106196

107197
setNotificationQueue((queue) => [...queue, item])
108198
},
109-
[chainName, getResumable, resumeTransactionFlow, t, queryClient],
199+
[chainName, client, getResumable, resumeTransactionFlow, t, queryClient],
110200
)
111201

112202
useCallbackOnTransaction(updateCallback)

src/transaction-flow/transaction/migrateProfile.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { getSubgraphRecords } from '@ensdomains/ensjs/subgraph'
77
import { setRecords } from '@ensdomains/ensjs/wallet'
88

99
import type { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
10-
import { bustMediaCache } from '@app/utils/metadataCache'
1110
import { profileRecordsToKeyValue, recordsWithCointypeCoins } from '@app/utils/records'
1211

1312
type Data = {
@@ -61,14 +60,6 @@ const transaction = async ({
6160
if (!profile) throw new Error('No profile found')
6261
const records = await profileRecordsToKeyValue(profile)
6362

64-
// Check if avatar or header are being migrated
65-
const hasAvatarChange = profile.texts?.some((t) => t.key === 'avatar')
66-
const hasHeaderChange = profile.texts?.some((t) => t.key === 'header')
67-
68-
// Bust cache for migrated media records
69-
if (hasAvatarChange) bustMediaCache(data.name, client, 'avatar')
70-
if (hasHeaderChange) bustMediaCache(data.name, client, 'header')
71-
7263
return setRecords.makeFunctionData(connectorClient, {
7364
name: data.name,
7465
resolverAddress,

src/transaction-flow/transaction/migrateProfileWithReset.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { getSubgraphRecords } from '@ensdomains/ensjs/subgraph'
77
import { setRecords } from '@ensdomains/ensjs/wallet'
88

99
import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
10-
import { bustMediaCache } from '@app/utils/metadataCache'
1110
import { profileRecordsToKeyValue, recordsWithCointypeCoins } from '@app/utils/records'
1211

1312
type Data = {
@@ -63,14 +62,6 @@ const transaction = async ({
6362
contract: 'ensPublicResolver',
6463
})
6564

66-
// Check if avatar or header are being migrated
67-
const hasAvatarChange = profile?.texts?.some((t) => t.key === 'avatar')
68-
const hasHeaderChange = profile?.texts?.some((t) => t.key === 'header')
69-
70-
// Bust cache for migrated media records
71-
if (hasAvatarChange) bustMediaCache(name, client, 'avatar')
72-
if (hasHeaderChange) bustMediaCache(name, client, 'header')
73-
7465
return setRecords.makeFunctionData(connectorClient, {
7566
name: data.name,
7667
...recordsWithCointypeCoins(profileRecords),

src/transaction-flow/transaction/resetProfile.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { Address } from 'viem'
44
import { clearRecords } from '@ensdomains/ensjs/wallet'
55

66
import type { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
7-
import { bustMediaCache } from '@app/utils/metadataCache'
87

98
type Data = {
109
name: string
@@ -30,12 +29,7 @@ const displayItems = ({ name, resolverAddress }: Data, t: TFunction): Transactio
3029
]
3130
}
3231

33-
const transaction = ({ client, connectorClient, data }: TransactionFunctionParameters<Data>) => {
34-
const { name } = data
35-
36-
// Bust cache for both avatar and header (resetting clears all records)
37-
bustMediaCache(name, client)
38-
32+
const transaction = ({ connectorClient, data }: TransactionFunctionParameters<Data>) => {
3933
return clearRecords.makeFunctionData(connectorClient, data)
4034
}
4135

src/transaction-flow/transaction/resetProfileWithRecords.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { RecordOptions } from '@ensdomains/ensjs/utils'
66
import { setRecords } from '@ensdomains/ensjs/wallet'
77

88
import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
9-
import { bustMediaCache } from '@app/utils/metadataCache'
109
import { recordOptionsToToupleList, recordsWithCointypeCoins } from '@app/utils/records'
1110

1211
type Data = {
@@ -52,17 +51,7 @@ const displayItems = ({ name, records }: Data, t: TFunction): TransactionDisplay
5251
]
5352
}
5453

55-
const transaction = ({ client, connectorClient, data }: TransactionFunctionParameters<Data>) => {
56-
const { name, records } = data
57-
58-
// Check if avatar or header are being set in the new records
59-
const hasAvatarChange = records.texts?.some((t) => t.key === 'avatar')
60-
const hasHeaderChange = records.texts?.some((t) => t.key === 'header')
61-
62-
// Bust cache for modified media records
63-
if (hasAvatarChange) bustMediaCache(name, client, 'avatar')
64-
if (hasHeaderChange) bustMediaCache(name, client, 'header')
65-
54+
const transaction = ({ connectorClient, data }: TransactionFunctionParameters<Data>) => {
6655
return setRecords.makeFunctionData(connectorClient, {
6756
name: data.name,
6857
...recordsWithCointypeCoins(data.records),

src/transaction-flow/transaction/updateProfile.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { RecordOptions } from '@ensdomains/ensjs/utils'
55
import { setRecords } from '@ensdomains/ensjs/wallet'
66

77
import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
8-
import { bustMediaCache } from '@app/utils/metadataCache'
98

109
import { recordOptionsToToupleList, recordsWithCointypeCoins } from '../../utils/records'
1110

@@ -59,17 +58,7 @@ const displayItems = ({ name, records }: Data, t: TFunction): TransactionDisplay
5958
]
6059
}
6160

62-
const transaction = ({ client, connectorClient, data }: TransactionFunctionParameters<Data>) => {
63-
const { name, records } = data
64-
65-
// Check if avatar or header are being modified in texts array
66-
const hasAvatarChange = records.texts?.some((t) => t.key === 'avatar')
67-
const hasHeaderChange = records.texts?.some((t) => t.key === 'header')
68-
69-
// Bust cache for modified media records
70-
if (hasAvatarChange) bustMediaCache(name, client, 'avatar')
71-
if (hasHeaderChange) bustMediaCache(name, client, 'header')
72-
61+
const transaction = ({ connectorClient, data }: TransactionFunctionParameters<Data>) => {
7362
return setRecords.makeFunctionData(connectorClient, {
7463
name: data.name,
7564
resolverAddress: data.resolverAddress,

src/transaction-flow/transaction/updateProfileRecords.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils'
1111
import { ProfileRecord } from '@app/constants/profileRecordOptions'
1212
import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
13-
import { bustMediaCache } from '@app/utils/metadataCache'
1413
import { recordOptionsToToupleList, recordsWithCointypeCoins } from '@app/utils/records'
1514

1615
type Data = {
@@ -82,14 +81,6 @@ const transaction = async ({
8281
const { name, resolverAddress, records, previousRecords = [], clearRecords } = data
8382
const submitRecords = getProfileRecordsDiff(records, previousRecords)
8483

85-
// Check if avatar or header are being modified
86-
const hasAvatarChange = submitRecords.some((r) => r.key === 'avatar' && r.group === 'media')
87-
const hasHeaderChange = submitRecords.some((r) => r.key === 'header' && r.group === 'media')
88-
89-
// Bust cache for modified media records
90-
if (hasAvatarChange) bustMediaCache(name, client, 'avatar')
91-
if (hasHeaderChange) bustMediaCache(name, client, 'header')
92-
9384
const recordOptions = await profileRecordsToRecordOptionsWithDeleteAbiArray(client, {
9485
name,
9586
profileRecords: submitRecords,

src/transaction-flow/transaction/updateResolver.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Address } from 'viem'
44
import { setResolver } from '@ensdomains/ensjs/wallet'
55

66
import { Transaction, TransactionDisplayItem, TransactionFunctionParameters } from '@app/types'
7-
import { bustMediaCache } from '@app/utils/metadataCache'
87

98
import { shortenAddress } from '../../utils/utils'
109

@@ -35,11 +34,7 @@ const displayItems = (
3534
},
3635
]
3736

38-
const transaction = ({ client, connectorClient, data }: TransactionFunctionParameters<Data>) => {
39-
// Bust cache for both avatar and header when changing resolver
40-
// The new resolver may have different records
41-
bustMediaCache(data.name, client)
42-
37+
const transaction = ({ connectorClient, data }: TransactionFunctionParameters<Data>) => {
4338
return setResolver.makeFunctionData(connectorClient, {
4439
name: data.name,
4540
contract: data.contract,

0 commit comments

Comments
 (0)