From eb34a59708c62731d78e2ba03481567b19d2a2d8 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 23 Apr 2025 11:27:43 -0700 Subject: [PATCH 1/3] Reinstate caching when scene is unmounted This also requires keeping track of the scene's last known fiat in case it changes while the scene is unmounted, which should result in a clearing of the cache. --- CHANGELOG.md | 1 + src/components/scenes/CoinRankingScene.tsx | 57 +++++++++++++++------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a2835dd4c..4e64d974689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - added: Moonpay Sell support for ACH. - added: Maestro `testID` support in `NotificationCard` - 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) diff --git a/src/components/scenes/CoinRankingScene.tsx b/src/components/scenes/CoinRankingScene.tsx index f8e5a828c17..d94f33ed84e 100644 --- a/src/components/scenes/CoinRankingScene.tsx +++ b/src/components/scenes/CoinRankingScene.tsx @@ -27,6 +27,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 @@ -54,6 +57,8 @@ 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)) @@ -61,7 +66,7 @@ const CoinRankingComponent = (props: Props) => { const lastStartIndex = React.useRef(1) const [requestDataSize, setRequestDataSize] = React.useState(QUERY_PAGE_SIZE) - const [dataSize, setDataSize] = React.useState(0) + const [dataSize, setDataSize] = React.useState(coinRankingDatas.length) const [searchText, setSearchText] = React.useState('') const [isSearching, setIsSearching] = React.useState(false) const [percentChangeTimeFrame, setPercentChangeTimeFrame] = React.useState('hours24') @@ -75,8 +80,6 @@ const CoinRankingComponent = (props: Props) => { [assetSubText, coingeckoFiat, percentChangeTimeFrame] ) - const { coinRankingDatas } = coinRanking - const renderItem = (itemObj: ListRenderItemInfo) => { const { index, item } = itemObj const currencyCode = coinRankingDatas[index]?.currencyCode ?? 'NO_CURRENCY_CODE' @@ -139,9 +142,22 @@ const CoinRankingComponent = (props: Props) => { }) React.useEffect(() => { + if (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 } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) React.useEffect(() => { @@ -156,8 +172,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) { @@ -166,6 +183,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 @@ -194,14 +212,14 @@ 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) @@ -209,15 +227,20 @@ const CoinRankingComponent = (props: Props) => { } 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 (mounted.current) { + 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 }, [coingeckoFiat /* reset subscription on fiat change */, queryLoop]) const listdata: number[] = React.useMemo(() => { From fe4152082cd26e403c85a88b1052edb28585cfd6 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 23 Apr 2025 10:59:44 -0700 Subject: [PATCH 2/3] Add a loader when list is empty --- CHANGELOG.md | 1 + src/components/scenes/CoinRankingScene.tsx | 29 +++++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e64d974689..b3577a142e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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 diff --git a/src/components/scenes/CoinRankingScene.tsx b/src/components/scenes/CoinRankingScene.tsx index d94f33ed84e..345e409e440 100644 --- a/src/components/scenes/CoinRankingScene.tsx +++ b/src/components/scenes/CoinRankingScene.tsx @@ -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' @@ -243,7 +244,7 @@ const CoinRankingComponent = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [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++) { @@ -305,17 +306,21 @@ const CoinRankingComponent = (props: Props) => { - + {listData.length === 0 ? ( + + ) : ( + + )} )} From 7c1c8d756655749ce359cd2def1f47e06e8a2fb9 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 25 Apr 2025 18:10:41 -0700 Subject: [PATCH 3/3] WIP --- src/components/scenes/CoinRankingScene.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/scenes/CoinRankingScene.tsx b/src/components/scenes/CoinRankingScene.tsx index 345e409e440..d2e3d7f242b 100644 --- a/src/components/scenes/CoinRankingScene.tsx +++ b/src/components/scenes/CoinRankingScene.tsx @@ -143,7 +143,7 @@ const CoinRankingComponent = (props: Props) => { }) React.useEffect(() => { - if (lastSceneFiat !== coingeckoFiat) { + 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 = [] @@ -230,7 +230,7 @@ const CoinRankingComponent = (props: Props) => { return () => { // Reset related query state when this effect is unmounted (coingeckoFiat // changed), but the scene is still mounted: - if (mounted.current) { + if (coingeckoFiat !== lastSceneFiat) { debugLog(LOG_COINRANK, `Clearing coinRankingDatas cache for new fiat: ${coingeckoFiat}`) clearTimeout(timeoutId) loopAbort()