Skip to content

Jon/fix/coinrank-loading #5544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
- added: Monero multi output support.
- added: Moonpay Sell support for ACH.
- added: Maestro `testID` support in `NotificationCard`
- added: Loader on `CoinRankingScene`
- fixed: `SceneWrapper` bottom inset calculations for scenes that do not `avoidKeyboard`
- fixed: Markets scene sometimes fails to load in iOS

## 4.26.0 (2025-04-14)

Expand Down
86 changes: 57 additions & 29 deletions src/components/scenes/CoinRankingScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { fetchRates } from '../../util/network'
import { EdgeAnim, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim'
import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity'
import { SceneWrapper } from '../common/SceneWrapper'
import { FillLoader } from '../progress-indicators/FillLoader'
import { CoinRankRow } from '../rows/CoinRankRow'
import { showDevError } from '../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../services/ThemeContext'
Expand All @@ -27,6 +28,9 @@ import { SearchFooter } from '../themed/SearchFooter'

const coinRanking: CoinRanking = { coinRankingDatas: [] }

/** Track changes that occurred to fiat while scene is unmounted */
let lastSceneFiat: string

const QUERY_PAGE_SIZE = 30
const LISTINGS_REFRESH_INTERVAL = 30000

Expand Down Expand Up @@ -54,14 +58,16 @@ const CoinRankingComponent = (props: Props) => {
const { navigation } = props
const dispatch = useDispatch()

const { coinRankingDatas } = coinRanking

/** The user's fiat setting, falling back to USD if not supported. */
const coingeckoFiat = useSelector(state => getCoingeckoFiat(state))

const mounted = React.useRef<boolean>(true)
const lastStartIndex = React.useRef<number>(1)

const [requestDataSize, setRequestDataSize] = React.useState<number>(QUERY_PAGE_SIZE)
const [dataSize, setDataSize] = React.useState<number>(0)
const [dataSize, setDataSize] = React.useState<number>(coinRankingDatas.length)
const [searchText, setSearchText] = React.useState<string>('')
const [isSearching, setIsSearching] = React.useState<boolean>(false)
const [percentChangeTimeFrame, setPercentChangeTimeFrame] = React.useState<PercentChangeTimeFrame>('hours24')
Expand All @@ -75,8 +81,6 @@ const CoinRankingComponent = (props: Props) => {
[assetSubText, coingeckoFiat, percentChangeTimeFrame]
)

const { coinRankingDatas } = coinRanking

const renderItem = (itemObj: ListRenderItemInfo<number>) => {
const { index, item } = itemObj
const currencyCode = coinRankingDatas[index]?.currencyCode ?? 'NO_CURRENCY_CODE'
Expand Down Expand Up @@ -139,9 +143,22 @@ const CoinRankingComponent = (props: Props) => {
})

React.useEffect(() => {
if (lastSceneFiat != null && lastSceneFiat !== coingeckoFiat) {
// Clear cache if we changed the fiat while outside of this scene
debugLog(LOG_COINRANK, 'Clearing coinRankingDatas cache')
coinRanking.coinRankingDatas = []
lastStartIndex.current = 1
setDataSize(0)
setRequestDataSize(QUERY_PAGE_SIZE)
}

return () => {
mounted.current = false

// Queries should update this, but just in case:
lastSceneFiat = coingeckoFiat
Comment on lines +158 to +159
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put this in a useUnmount hook?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment
#5544 (comment)

We need to pass mounted.current around separately anyway, so might as well fold
the changes into this existing effect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why I suggest useUnmount is to avoid the disabled eslint rule hack.

}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

React.useEffect(() => {
Expand All @@ -156,8 +173,9 @@ const CoinRankingComponent = (props: Props) => {
debugLog(LOG_COINRANK, `queryLoop(start: ${startIndex})`)

try {
// Catch up to the total required items
while (startIndex < requestDataSize - QUERY_PAGE_SIZE) {
// Catch up to the total required items. Always fetch if requesting the
// first page.
while (startIndex === 1 || startIndex < requestDataSize - QUERY_PAGE_SIZE) {
const url = `v2/coinrank?fiatCode=iso:${coingeckoFiat}&start=${startIndex}&length=${QUERY_PAGE_SIZE}`
const response = await fetchRates(url).then(maybeAbort)
if (!response.ok) {
Expand All @@ -166,6 +184,7 @@ const CoinRankingComponent = (props: Props) => {
break
}
const replyJson = await response.json().then(maybeAbort)
lastSceneFiat = coingeckoFiat
const listings = asCoinranking(replyJson)
for (let i = 0; i < listings.data.length; i++) {
const rankIndex = startIndex - 1 + i
Expand Down Expand Up @@ -194,33 +213,38 @@ const CoinRankingComponent = (props: Props) => {

// Subscribe to changes to the current data set:
React.useEffect(() => {
let abort = () => {}
let loopAbort = () => {}
// Refresh from the beginning periodically
let timeoutId = setTimeout(loopBody, LISTINGS_REFRESH_INTERVAL)
function loopBody() {
debugLog(LOG_COINRANK, 'Refreshing list')
const abortable = queryLoop(1)
abort = abortable.abort
abortable.promise
const abortableQueryLoop = queryLoop(1)
loopAbort = abortableQueryLoop.abort
abortableQueryLoop.promise
.catch(e => console.error(`Error in query loop: ${e.message}`))
.finally(() => {
timeoutId = setTimeout(loopBody, LISTINGS_REFRESH_INTERVAL)
})
}

return () => {
// Reset related query state when this effect is unmounted:
clearTimeout(timeoutId)
abort()
pageQueryAbortRef.current()
coinRanking.coinRankingDatas = []
lastStartIndex.current = 1
setDataSize(0)
setRequestDataSize(QUERY_PAGE_SIZE)
// Reset related query state when this effect is unmounted (coingeckoFiat
// changed), but the scene is still mounted:
if (coingeckoFiat !== lastSceneFiat) {
debugLog(LOG_COINRANK, `Clearing coinRankingDatas cache for new fiat: ${coingeckoFiat}`)
clearTimeout(timeoutId)
loopAbort()
pageQueryAbortRef.current()
coinRanking.coinRankingDatas = []
lastStartIndex.current = 1
setDataSize(0)
setRequestDataSize(QUERY_PAGE_SIZE)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this // eslint-disable-next-line react-hooks/exhaustive-deps added? Is there an alternative?

Copy link
Collaborator Author

@Jon-edge Jon-edge Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the cause of the actual bug in the task - this cleanup fn is appropriate only for when the scene is still mounted, implying that the fiat changed while the scene was mounted, so clearing everything was appropriate.

It's inappropriate to run this cleanup when the scene unmounts, which was happening in the original implementation. We lose the cache in that case, which defeats the purpose of the cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for the eslint-disable-next-line rule hack though? Seeing this hack is a red flag. I'm trying to understand why it's necessary and perhaps why another approach wasn't taken.

}, [coingeckoFiat /* reset subscription on fiat change */, queryLoop])

const listdata: number[] = React.useMemo(() => {
const listData: number[] = React.useMemo(() => {
debugLog(LOG_COINRANK, `Updating listdata dataSize=${dataSize} searchText=${searchText}`)
const out = []
for (let i = 0; i < dataSize; i++) {
Expand Down Expand Up @@ -282,17 +306,21 @@ const CoinRankingComponent = (props: Props) => {
</View>
<DividerLine marginRem={[0, 0, 0, 1]} />
<View style={{ ...undoInsetStyle, marginTop: 0 }}>
<Animated.FlatList
contentContainerStyle={{ ...insetStyle, paddingTop: 0 }}
data={listdata}
extraData={extraData}
keyboardDismissMode="on-drag"
onEndReachedThreshold={1}
onEndReached={handleEndReached}
onScroll={handleScroll}
renderItem={renderItem}
scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX}
/>
{listData.length === 0 ? (
<FillLoader />
) : (
<Animated.FlatList
contentContainerStyle={{ ...insetStyle, paddingTop: 0 }}
data={listData}
extraData={extraData}
keyboardDismissMode="on-drag"
onEndReachedThreshold={1}
onEndReached={handleEndReached}
onScroll={handleScroll}
renderItem={renderItem}
scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX}
/>
)}
</View>
</>
)}
Expand Down
Loading