-
Notifications
You must be signed in to change notification settings - Fork 279
Jon/fix/markets-fiat-change #5512
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
Conversation
1a9d91b to
870421c
Compare
samholmes
left a comment
There was a problem hiding this 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.
2e44072 to
42220d7
Compare
samholmes
left a comment
There was a problem hiding this 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.
There was a problem hiding this comment.
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).
| debugLog(LOG_COINRANK, `handleEndReached. setRequestDataSize ${dataSize + QUERY_PAGE_SIZE}`) | ||
| setRequestDataSize(dataSize + QUERY_PAGE_SIZE) |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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.
| // 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}`)) | ||
| } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
| if (!isHandlingFiatChange && lastFetchedFiat.current !== currentFiat) { | ||
| debugLog(LOG_COINRANK, `Stopping stale query loop for ${lastFetchedFiat.current} due to fiat change`) | ||
| dataUpdated = false | ||
| break | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (abort) return
| if (isHandlingFiatChange) { | ||
| setForceRefresh(prev => prev + 1) | ||
| } |
There was a problem hiding this comment.
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.
| () => ({ assetSubText, supportedFiatSetting: coingeckoFiat, percentChangeTimeFrame, forceRefresh }), | ||
| [assetSubText, coingeckoFiat, percentChangeTimeFrame, forceRefresh] |
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already handling what?
2e65396 to
d3bef26
Compare
e6be2f1 to
92ce3a3
Compare
92ce3a3 to
7b8fdd9
Compare
samholmes
left a comment
There was a problem hiding this 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)', |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
7b8fdd9 to
bad644e
Compare
939b89b to
0dc8a35
Compare
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).
0dc8a35 to
4dc4071
Compare
handleOnEndReachedhandling after changing fiatCHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
noneRequirements
If you have made any visual changes to the GUI. Make sure you have: