If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Action Performed:
Here is a very minimal reproduction of this bug in the rn-tester sample project, that's not dependent upon any new code in the React Native codebase. Run the rn-tester app, find the ScrollView example, and observe the following behavior:
If minIndexForVisible is 0, then the scroll position will be maintained iff:
- The first item in the list is not visible, AND
- The current
contentOffset is at least y, where y is the height of the new content being prepended
Similarly, if minIndexForVisible is 1, then the scroll position will be maintained iff:
- The second item in the list is not visible, AND
- The current
contentOffset is at least y, where y is the height of the new content being prepended
And so on... If either of those conditions are not met, the scroll position will not be maintained.
Expected Result:
As long as the list has enough content that it is scrollable, the contentOffset should be adjusted such that the scroll position is maintained when new items are added to the start or end of the list.
Actual Result:
The contentOffset is not adjusted, and the scroll position is not maintained.
Additional Details
Background
How does the maintainVisibleContentPosition prop work?
At a high level, when new UI elements are added to the start of the list, React Native will:
- Measure the position of the first visible item before the new items are added
- Add the new items to the view.
- Measure the difference in position between the first visible item found in step 1 with its new position.
- Increase the
contentOffset of the scroll container by the amount calculated in the previous step, such that:
- The same first item is visible before and after adding the new items to the list, and
- All the newly-prepended items are out of view, and further towards the start of the list.
However, this prop does not work consistently – sometimes in step 3 the difference in position is incorrectly calculated to be zero. Furthermore, we have noticed that this seems only to happen consistently when the content length of newly-prepended list items is long.
Motivation
For a few months now, we have been endeavoring to get a working solution for a bidirectional-scrolling virtualized list in React Native. After working through many potential solutions, we have come very close to a working solution directly in React Native's VirtualizedList through the code in this PR. However, after lots of debugging we determined that the issues we were seeing weren't caused by the JS / VirtualizedList at all, but instead by this bug in ScrollView's maintainVisibleContentPosition prop, which is implemented in the native layer.
The result of this bug is that our implementation of the onStartReached prop in VirtualizedList suffers from the following issue:
- When you reach the start of the list (
contentOffset of 0), onStartReached is called.
- The callback to
onStartReached prepends new items into the list.
maintainVisibleContentPosition fails to update the contentOffset to account for those new list items.
- The new list items are rendered, but the
contentOffset is still 0, so the list position jumps to the start of the new content.
- Because the
contentOffset is 0, onStartReached is called again, and we get an infinite loop (at least, until there's no more content to load).
Android considerations
Another important piece of information is that the maintainVisibleContentPosition prop is not yet available on Android (implementation in progress). We have examined the in-progress Android implementation and found that it is very similar to the iOS one, and likely shares the same problem.
For the sake of this issue, the scope is focused on iOS, but we believe that the solution in one platform will be applicable in the other.
Potential cause
According to review comments from a Meta engineer, this bug is likely caused by a race condition between the items being added to the list and content offset being adjusted.
They also suggest implementing a binary search for the first visible item, which seems like it might improve the issue, but (in my opinion) is unlikely to resolve the race condition entirely.
Evidence of potential race condition?
In the FlatList example linked above, if you tweak these parameters as follows:
const PAGE_SIZE = 10;
const INITIAL_PAGE_OFFSET = 50;
const NUM_PAGES = 100;
The problem is mitigated but not completely solved (a few pages load before maintainVisibleContentPosition seems to "catch up" and function as expected). This hints that the problem may indeed be a race condition as suggested above.
autoScrollToTopThreshold wonkiness
According to the React Native documentation:
The optional autoscrollToTopThreshold can be used to make the content automatically scroll to the top after making the adjustment if the user was within the threshold of the top before the adjustment was made.
This suggests that with an autoScrollToTopThreshold of 0, then no auto-scrolling should occur if you have a non-zero contentOffset before new items are appended to the list. We have observed that this is not the case by:
- Scrolling down a few pixels (without taking the
minIndexForVisible item out of view)
- Prepending an item.
Despite having an autoScrollToTopThreshold of 0 and a non-zero contentOffset, the ScrollView auto-scrolls to the top.
Interestingly, this particular ScrollView example listed in the reproduction steps can be fixed by removing the autoScrollToTopThreshold parameter entirely. While this might be a hint at how to solve this, it does not seem to be a viable solution for us. Even without an autoScrollToTopThreshold parameter, the same problem occurs in this FlatList example. It's unclear why removing the autoScrollToTopThreshold parameter fixes the problem, but setting a value of 0 does not. 🤔
Workaround:
While there may be workarounds possible via hacks in the JS code involving setTimeout or extra calls to scrollToIndex/scrollToOffset, these would not solve the root problem. In order to have a proposal accepted, it must fix the problem in the React Native codebase itself, probably in the native layer.
Platform:
Right now, this problem has been confirmed on iOS. It very likely exists on Android as well in this implementation. We are working on confirming the issue there, and will be following the progress of that pull request.
For the scope of this issue, we'll only require a fix in iOS or Android (preferably iOS), submitted as a PR against the our react-native fork: https://github.com/Expensify/react-native. Applying the same fix in the other platform should be comparatively easy and can be treated as a follow-up.
View all open jobs on GitHub
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~01fe321bebf9b78f69
- Upwork Job ID: 1606337510652706816
- Last Price Increase: 2022-12-23
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Action Performed:
Here is a very minimal reproduction of this bug in the rn-tester sample project, that's not dependent upon any new code in the React Native codebase. Run the rn-tester app, find the
ScrollViewexample, and observe the following behavior:If
minIndexForVisibleis0, then the scroll position will be maintained iff:contentOffsetis at leasty, whereyis the height of the new content being prependedSimilarly, if
minIndexForVisibleis1, then the scroll position will be maintained iff:contentOffsetis at leasty, whereyis the height of the new content being prependedAnd so on... If either of those conditions are not met, the scroll position will not be maintained.
Expected Result:
As long as the list has enough content that it is scrollable, the
contentOffsetshould be adjusted such that the scroll position is maintained when new items are added to the start or end of the list.Actual Result:
The
contentOffsetis not adjusted, and the scroll position is not maintained.Additional Details
Background
How does the
maintainVisibleContentPositionprop work?At a high level, when new UI elements are added to the start of the list, React Native will:
contentOffsetof the scroll container by the amount calculated in the previous step, such that:However, this prop does not work consistently – sometimes in step 3 the difference in position is incorrectly calculated to be zero. Furthermore, we have noticed that this seems only to happen consistently when the content length of newly-prepended list items is long.
Motivation
For a few months now, we have been endeavoring to get a working solution for a bidirectional-scrolling virtualized list in React Native. After working through many potential solutions, we have come very close to a working solution directly in React Native's VirtualizedList through the code in this PR. However, after lots of debugging we determined that the issues we were seeing weren't caused by the JS /
VirtualizedListat all, but instead by this bug in ScrollView'smaintainVisibleContentPositionprop, which is implemented in the native layer.The result of this bug is that our implementation of the
onStartReachedprop inVirtualizedListsuffers from the following issue:contentOffsetof0),onStartReachedis called.onStartReachedprepends new items into the list.maintainVisibleContentPositionfails to update thecontentOffsetto account for those new list items.contentOffsetis still0, so the list position jumps to the start of the new content.contentOffsetis0,onStartReachedis called again, and we get an infinite loop (at least, until there's no more content to load).Android considerations
Another important piece of information is that the
maintainVisibleContentPositionprop is not yet available on Android (implementation in progress). We have examined the in-progress Android implementation and found that it is very similar to the iOS one, and likely shares the same problem.For the sake of this issue, the scope is focused on
iOS, but we believe that the solution in one platform will be applicable in the other.Potential cause
According to review comments from a Meta engineer, this bug is likely caused by a race condition between the items being added to the list and content offset being adjusted.
They also suggest implementing a binary search for the first visible item, which seems like it might improve the issue, but (in my opinion) is unlikely to resolve the race condition entirely.
Evidence of potential race condition?
In the FlatList example linked above, if you tweak these parameters as follows:
The problem is mitigated but not completely solved (a few pages load before
maintainVisibleContentPositionseems to "catch up" and function as expected). This hints that the problem may indeed be a race condition as suggested above.autoScrollToTopThresholdwonkinessAccording to the React Native documentation:
This suggests that with an
autoScrollToTopThresholdof0, then no auto-scrolling should occur if you have a non-zerocontentOffsetbefore new items are appended to the list. We have observed that this is not the case by:minIndexForVisibleitem out of view)Despite having an
autoScrollToTopThresholdof0and a non-zerocontentOffset, theScrollViewauto-scrolls to the top.Interestingly, this particular
ScrollViewexample listed in the reproduction steps can be fixed by removing theautoScrollToTopThresholdparameter entirely. While this might be a hint at how to solve this, it does not seem to be a viable solution for us. Even without anautoScrollToTopThresholdparameter, the same problem occurs in this FlatList example. It's unclear why removing theautoScrollToTopThresholdparameter fixes the problem, but setting a value of0does not. 🤔Workaround:
While there may be workarounds possible via hacks in the JS code involving
setTimeoutor extra calls toscrollToIndex/scrollToOffset, these would not solve the root problem. In order to have a proposal accepted, it must fix the problem in the React Native codebase itself, probably in the native layer.Platform:
Right now, this problem has been confirmed on iOS. It very likely exists on Android as well in this implementation. We are working on confirming the issue there, and will be following the progress of that pull request.
For the scope of this issue, we'll only require a fix in iOS or Android (preferably iOS), submitted as a PR against the our react-native fork: https://github.com/Expensify/react-native. Applying the same fix in the other platform should be comparatively easy and can be treated as a follow-up.
View all open jobs on GitHub
Upwork Automation - Do Not Edit