Skip to content

Conversation

@Jon-edge
Copy link
Collaborator

@Jon-edge Jon-edge commented Mar 22, 2025

  • Race conditions
  • Multiple fetches for the same data
  • Stale periodic task timers retaining the old fiat
  • Fetch indexing bugs after changing fiat
  • Failure to re-render due to global data cache
  • handleOnEndReached handling after changing fiat

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch from 1a9d91b to 870421c Compare March 24, 2025 17:28
Copy link
Contributor

@samholmes samholmes left a comment

Choose a reason for hiding this comment

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

Last commit should be broken into multiple commits for each change in the itemized list in the commit body.

I feel like the code is decaying because we're dealing with unconventional use of React's API. Instead we could consider improving the implementation to fix the issues without the increasing overhead of tech-debt. Though, it's not clear what that may look like until the changes are cleared up by breaking up the commit.

@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch 2 times, most recently from 2e44072 to 42220d7 Compare March 31, 2025 21:25
Copy link
Contributor

@samholmes samholmes left a comment

Choose a reason for hiding this comment

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

I have a strong feeling that this approach is a shotgun approach to solving the issue. The amount of time spent with the shotgun may have been equal or less time if spent with a handgun (refract the imperative mess) or a sniper riffle (surgical change to get it to work). The reason why I say this is because it seems like using a ref for the query function is so that way it prevents it from being a closure, but then it wants a closure on some previous state values. I feel like the right surgical change here is to pick one approach: is it a closure or is it not a closure?

From what I can tell, it has to be a closure in order to abort it, so that's one reason to keep it as a closure. That would greatly simplify things (no more useHandler), no more forceRefresh. If it's no longer a closure (e.g. it's a handler from useHandler), then I'm not sure how to abort it, so it's effects should be noop'd at the end of it's routine (not ideal). So I say keep it a closure and see what you can do with that constraint. I think we'll quickly see this PR line changes go down significantly and the change as a whole be a lot more straight forward.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like an optional change. Potentially drop this commit to reduce adding to the tech debt that is in this component. (We should clean this component up later by removing the dangling state).

Comment on lines 113 to 114
debugLog(LOG_COINRANK, `handleEndReached. setRequestDataSize ${dataSize + QUERY_PAGE_SIZE}`)
setRequestDataSize(dataSize + QUERY_PAGE_SIZE)
Copy link
Contributor

Choose a reason for hiding this comment

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

Might not be correct. Give valid reason for why we're deriving requestDataSize from dataSize

// the first index (initial load/timed refresh)
const queryLoop = async (startIndex: number) => {
// Define the query function that will be stored in the ref
queryLoopRef.current = async (startIndex: number) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

useHandler is good for this.

Comment on lines 250 to 220
// Effect to handle changes in requestDataSize and coingeckoFiat
React.useEffect(() => {
// Always clear any existing timeout when making changes to the query
if (timeoutHandler.current) {
clearTimeout(timeoutHandler.current)
timeoutHandler.current = undefined
}

if (lastFetchedFiat.current !== coingeckoFiat) {
// Reset everything when the fiat setting does not match what cached
// data we fetched previously.
debugLog(LOG_COINRANK, `Fiat changed from ${lastFetchedFiat.current} to ${coingeckoFiat}`)
isQuerying.current = false
coinRanking.coinRankingDatas = []
lastStartIndex.current = 1
setDataSize(0)
setRequestDataSize(QUERY_PAGE_SIZE)

// Force a re-render immediately to ensure no stale data is shown while we wait for fetch
setForceRefresh(prev => prev + 1)

lastFetchedFiat.current = coingeckoFiat
}

// Trigger a query with the current parameters
if (queryLoopRef.current) {
queryLoopRef.current(lastStartIndex.current).catch(e => console.error(`Error in query loop: ${e.message}`))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

After lengthy discussion with jon, I understand why this is needed, and it's a side-effect of how the component is implemented. Later, we should clean this up.

lastStartIndex.current = startIndex
// Do nothing if fiat was changed while the fetch was happening. There is
// another call to queryLoop() handling this.
if (lastFetchedFiat.current !== currentFiat && !isHandlingFiatChange) return
Copy link
Contributor

Choose a reason for hiding this comment

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

You can abort routines with this pattern:

useEffect(() => {
  let abort = false
  const cb = () => {
    await something()
    if (abort) return
    await somethingElse()
    if (abort) return
    setState(...)
  }

  cb().catch(...)
  
  return () => abort = true
})

Now you can make cb have closure over whatever state was available during it's declaration. If a useEffect(callback)'s callback is called, it'll abort the previous subscription and instantiate a new subscription.

Remember: useEffect should have been named useSubscription (see David Khourshid's excellent presentation on this).

Comment on lines 217 to 212
if (!isHandlingFiatChange && lastFetchedFiat.current !== currentFiat) {
debugLog(LOG_COINRANK, `Stopping stale query loop for ${lastFetchedFiat.current} due to fiat change`)
dataUpdated = false
break
}
Copy link
Contributor

Choose a reason for hiding this comment

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

if (abort) return

Comment on lines 261 to 253
if (isHandlingFiatChange) {
setForceRefresh(prev => prev + 1)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Whenever you see this pattern of a counter to force a refresh, you can instead ask "What condition forces the refresh?" "Can that condition be used as a dependency?" If yes, then use the dependency for the refresh:

useEffect(callback, [coingeckoFiat])

There, now whenever coingeckoFiat changes (which is you're condition), we'll invoke callback. Now we have a refresh without extra state forceRefresh.

Comment on lines 80 to 83
() => ({ assetSubText, supportedFiatSetting: coingeckoFiat, percentChangeTimeFrame, forceRefresh }),
[assetSubText, coingeckoFiat, percentChangeTimeFrame, forceRefresh]
Copy link
Contributor

Choose a reason for hiding this comment

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

It wasn't clear how this forceRefresh will be used in a later commit (which is why it's generally best to include usage and implementation in the same commit, but I digress), so it seems forceRefresh is only updated when coingeckFiat changes, so this is a redundant dependency for extraData memoization.

Comment on lines 309 to 300
// Always clear any existing timeout when making changes to the query and an
// existing timeout is in flight. Running this effect either to grab extra
// rows or change fiat always triggers a new query immediately.
Copy link
Contributor

Choose a reason for hiding this comment

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

So then why isn't it in the cleanup function?

timeoutHandler.current = undefined

if (lastFetchedFiat.current !== coingeckoFiat) {
if (coinRanking.coinRankingDatas.length === 0) return // Already handling
Copy link
Contributor

Choose a reason for hiding this comment

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

Already handling what?

@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch 2 times, most recently from 2e65396 to d3bef26 Compare April 1, 2025 21:54
@samholmes samholmes force-pushed the jon/fix/markets-fiat-change branch 2 times, most recently from e6be2f1 to 92ce3a3 Compare April 2, 2025 18:01
@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch from 92ce3a3 to 7b8fdd9 Compare April 2, 2025 21:20
Copy link
Contributor

@samholmes samholmes left a comment

Choose a reason for hiding this comment

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

Looking good. Just some extra comments from what I noticed while reviewing on my phone.

I’m curious to know how the testing went too.

type: 'problem',
docs: {
description: 'Ensure awaited promises within useCancellable are followed by .then(safety)',
description: 'Ensure awaited promises within useAbortable are followed by .then(abort)',
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be distinct from abort() which is the function that aborts the routine? Instead this is a function that checks if the routine has aborted. What do you think of checkAbort?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think maybeAbort is better. "Check" doesn't communicate what happens as a result of that check, just says that the abort condition is being checked. "Abort" is what's potentially going to happen.

const pageQueryCancelRef = React.useRef<() => void>(() => {})
React.useEffect(() => {
const { promise, cancel } = queryLoop(lastStartIndex.current)
const { promise, abort: cancel } = queryLoop(lastStartIndex.current)
Copy link
Contributor

Choose a reason for hiding this comment

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

Missed the words “cancel” in this file. Do a quick find replace with case preserved to fix the issue.

@samholmes samholmes force-pushed the jon/fix/markets-fiat-change branch from 7b8fdd9 to bad644e Compare April 3, 2025 15:41
@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch 2 times, most recently from 939b89b to 0dc8a35 Compare April 3, 2025 19:53
Jon-edge and others added 6 commits April 4, 2025 15:03
This separates the subscription effect from the pagination effect.
With this separation it's clear what dependencies are required for each
effect.

The `queryLoop` function is not longer responsible for the loop; that's
a concern for the subscription effect. The `queryLoop` is now just a
query function and could be renamed.

Also, the `queryLoop` is a "handler" so it can close-over the
component's state completely.
This is more effective because it allows us to abort a specific routine
given a reference to an abort handler function. The reason for this is
to remove concern regarding shared state for aborting routines
(`isQuerying` was shared between separate routine invocations).
@Jon-edge Jon-edge force-pushed the jon/fix/markets-fiat-change branch from 0dc8a35 to 4dc4071 Compare April 4, 2025 22:35
@Jon-edge Jon-edge merged commit d9b0e55 into develop Apr 4, 2025
2 checks passed
@Jon-edge Jon-edge deleted the jon/fix/markets-fiat-change branch April 4, 2025 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants