Skip to content
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

Fix a bug with ListView with sticky headers + RefreshControl #5445

Closed

Conversation

janicduplessis
Copy link
Contributor

The bug is caused by a weird race condition. What happens is that when calling UIRefreshControl#endRefreshing the UIScrollView delegate scrollViewDidScroll function is called synchronously and then dockClosestSectionHeader crashes because the sticky header indexes are updated but not the contentView children.

I fixed it by adding an updating property on RCTRefreshControl and setting it before calling endRefreshing so we can know not to call dockClosestSectionHeader at that moment.

Tested with both RefreshControl and onRefreshStart prop.

I reproduced the bug by replacing ListViewExample.js in UIExplorer with https://gist.github.com/janicduplessis/05fc58e852f3e80e51b9

Fixes #5440

cc @nicklockwood

@facebook-github-bot
Copy link
Contributor

By analyzing the blame information on this pull request, we identified @nicklockwood, @sahrens and @tadeuzagallo to be potential reviewers.

@facebook-github-bot facebook-github-bot added GH Review: review-needed CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. labels Jan 20, 2016
@facebook-github-bot
Copy link
Contributor

@janicduplessis updated the pull request.

@facebook-github-bot
Copy link
Contributor

@janicduplessis updated the pull request.

@janicduplessis
Copy link
Contributor Author

Ping @nicklockwood. Could you review this when you get some time, it is the last of my RefreshControl bug fix streak :)

Updated with the little fix I didn't get to commit in #5745.

@nicklockwood
Copy link
Contributor

We've actually got an intermittent error due to sticky header indices being updated out of step with the subviews anyway, so it might be better to fix the problem at its source rather than add this workaround. I'll look into that today.

@janicduplessis
Copy link
Contributor Author

Ok great!

@janicduplessis
Copy link
Contributor Author

I thought about that a bit and since stickyHeaderIndices and children are tightly coupled the order for which props setter functions are called become important. Maybe we could remove the stickyHeaderIndices prop from the native ScrollView and add a prop or something to the child sticky header views to be able to differentiate them in native. Now there would be only 1 source so there won't be sync issues anymore.

@nicklockwood
Copy link
Contributor

Maybe we could remove the stickyHeaderIndices prop from the native ScrollView and add a prop or something to the child sticky header views to be able to differentiate them in native.

Sticky headers can be any kind of view, so the only way to do that would be to add the "isSticky" property to <View>, which seems ugly. The alternative would be to create a new <ScrollViewStickyHeaderView>, and require that headers be wrapped in it, but I'm not hugely keen on that approach either.

@janicduplessis
Copy link
Contributor Author

I meant changing only the native ScrollView API not the js wrapper. Did you have any other idea on how to fix this?

@nicklockwood
Copy link
Contributor

@janicduplessis We don't pass the props of subviews to the parent on the native side, and any props that aren't explicitly exported by the viewmanager are never sent to the native side, so I'm not sure how you'd implement it on the native side without exporting an isStickyHeader prop from RCTViewManager.

@nicklockwood
Copy link
Contributor

My plan to fix it is simply to suppress the warning on scroll, since it's sometimes a false positive. Warning on batchDidComplete should be sufficient, since props and subviews are supposed to be in sync at that point.

@janicduplessis
Copy link
Contributor Author

Yea you are right that wouldn't work, removing the warning on scroll and just not updating the sticky headers if the indices don't match sounds like a good plan.

…ting refreshing to false on a RefreshControl
@facebook-github-bot
Copy link
Contributor

@janicduplessis updated the pull request.

@janicduplessis
Copy link
Contributor Author

@nicklockwood Does that looks good to you? I pretty much just removed the error and it still fixes the issue. I left the little contentOffset tweak and changed the class in ScollView to use RCTRefreshControl instead of UIRefreshControl.

if (idx >= subviewCount) {
RCTLogError(@"Sticky header index %zd was outside the range {0, %zd}", idx, subviewCount);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we keep the warning for now, but just downgrade it to RCTLogWarn? I'd rather keep it until we're sure of what causes it (there are other cases besides pull to refresh).

Also can you add *stop = YES; before the return as well? Just so we don't generate repeat warnings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nicklockwood Maybe I could validate the indices in bridgeDidFinishTransaction so we can keep the warning if the indices passed are actually bad but not warn when they are out of sync when called from scrollViewDidScroll. I wanted to move the warning to js initially but the children array in js was pretty different that the reactSubviews in native (reactSubview seems to be flattened somehow).

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd be fine with that, but still change it to RCTLogWarn, as we're occasionally seeing this error happening with bridgeDidFinishTransaction in the stack trace, so clearly there is at least one scenario where it arises that doesn't relate to scroll event race conditions.

@facebook-github-bot
Copy link
Contributor

@janicduplessis updated the pull request.

@janicduplessis
Copy link
Contributor Author

The warning does spam a lot if there actually are out of range indices but I guess it's not too bad. Also should I do the check only in debug builds so it doesn't affect perf on release since the check is done on every frame?

@nicklockwood
Copy link
Contributor

@facebook-github-bot shipit

@facebook-github-bot
Copy link
Contributor

Thanks for importing. If you are an FB employee go to Phabricator to review.

@ghost ghost closed this in 671b975 Feb 19, 2016
pglotov pushed a commit to pglotov/react-native that referenced this pull request Mar 15, 2016
Summary:The bug is caused by a weird race condition. What happens is that when calling `UIRefreshControl#endRefreshing` the `UIScrollView` delegate `scrollViewDidScroll` function is called synchronously and then `dockClosestSectionHeader` crashes because the sticky header indexes are updated but not the contentView children.

I fixed it by adding an updating property on `RCTRefreshControl` and setting it before calling `endRefreshing` so we can know not to call `dockClosestSectionHeader` at that moment.

Tested with both `RefreshControl` and `onRefreshStart` prop.

I reproduced the bug by replacing ListViewExample.js in UIExplorer with https://gist.github.com/janicduplessis/05fc58e852f3e80e51b9

Fixes facebook#5440

cc nicklockwood
Closes facebook#5445

Differential Revision: D2953984

Pulled By: nicklockwood

fb-gh-sync-id: c17a6a75ab31ef54d478706ed17a8115a11d734e
shipit-source-id: c17a6a75ab31ef54d478706ed17a8115a11d734e
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
3 participants