react-native icon indicating copy to clipboard operation
react-native copied to clipboard

feat: make view recycling optional on iOS

Open WoLewicki opened this issue 1 year ago • 18 comments

Summary

This PR resolves the potential problem of misconfiguration of components after being recycled. Some of them have custom, sometimes native (e.g. connected to VCs) logic that messes up with the concept of recycling.

Changelog

Added shouldBeRecycled method to RCTComponentViewProtocol, a check for it in _enqueueComponentViewWithComponentHandle:(ComponentHandle)componentHandle componentViewDescriptor:(RCTComponentViewDescriptor)componentViewDescriptor method, and a default implementation in UIView+ComponentViewProtocol returning YES in order not to change the default behavior.

[iOS] [Added] - Add shouldBeRecycled method on iOS.

Test Plan

Override this method in your custom componentView and see that the component is not recycled.

WoLewicki avatar Nov 17 '22 16:11 WoLewicki

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 8,842,749 +3
android hermes armeabi-v7a 8,152,151 +4
android hermes x86 9,348,439 +4
android hermes x86_64 9,191,273 +4
android jsc arm64-v8a 9,454,435 +4
android jsc armeabi-v7a 8,635,756 +2
android jsc x86 9,537,431 +5
android jsc x86_64 9,780,793 +4

Base commit: f6c417c7ae221b4b54f954008c29558290c7744e Branch: main

analysis-bot avatar Nov 17 '22 16:11 analysis-bot

Platform Engine Arch Size (bytes) Diff
ios - universal n/a --

Base commit: 0062b10b56985c4556011fbbb8d43f0a038d359e Branch: main

analysis-bot avatar Nov 17 '22 17:11 analysis-bot

PR build artifact for 46a905b78eb3cffbd72d3256e09900298eee1f44 is ready. To use, download tarball from "Artifacts" tab in this CircleCI job then run yarn add <path to tarball> in your React Native project.

pull-bot avatar Nov 17 '22 17:11 pull-bot

@mdvacca has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

facebook-github-bot avatar Nov 17 '22 18:11 facebook-github-bot

Hello @WoLewicki,

It is possible to re-create native resource inside prepareForRecycle to work around any recycling issues. One could re-create UIViewController inside prepareForRecycle. Would that work for you? This avoids adding an exception to the logic but allows component to opt out of recycling.

sammy-SC avatar Nov 18 '22 12:11 sammy-SC

@sammy-SC If I remember correctly the issue was that a view cannot be added as the main view (VC.view property) of multiple view controllers. Once it has been added to one it cannot be added again, with no way that I found to clear that, which is where recycling is problematic.

janicduplessis avatar Nov 18 '22 15:11 janicduplessis

Hey @sammy-SC, in case of UIViewController, used by react-native-screens, iirc there were some problems with recreating view controllers and attaching screens to it since it messed with the native screen transitions. I cannot precisely recall the exact problem, but we tried that approach and it created some confusing native VC states. I believe it was due to the fact that native screen transition in react-native-screens is triggered by the change of React children, which also triggers prepareForRecycle method, so removing VC in that moment is not a good idea.

WoLewicki avatar Nov 18 '22 16:11 WoLewicki

Hey, I’m the author of the Navigation router. It took me more than 6 months to migrate my library to the new architecture. I can remember spending a couple of those weeks just trying to get view recycling working with UIViewController-backed views (like scenes and tab bars).

I finally found a pattern that works. I don’t clear the UIViewController in prepareForRecycle. Instead I set a flag that indicates it should be cleared. And then when the view is used again, in updateProps or mountChildComponentView, I do that actual clearing. @WoLewicki can you give this a go and see if it works for you, please?

Recycling views was a big headache for contentInsetAdjustmentBehavior set to automatic, too. It took me another solid week to get prepareForRecycle working with large titles

grahammendick avatar Nov 20 '22 14:11 grahammendick

Hey @grahammendick, thanks for adding more context to it. I currently do not work on react-native-screens, so it would better to cc @kkafar I believe. Nevertheless, I think that merging this PR and removing all those hacky solutions is the way to go. wdyt of it?

WoLewicki avatar Nov 21 '22 10:11 WoLewicki

I support this PR because view recycling is a massive pain for integrating with the native api on iOS. I'm still not 100% sure the pattern I settled on works in all scenarios.

But I think this PR should also override the shouldBeRecycled on the RCTScrollViewComponentView and return false if contentInsetAdjustmentBehavior is automatic; and back out the associated hack

grahammendick avatar Nov 21 '22 12:11 grahammendick

But I think this PR should also override the shouldBeRecycled on the RCTScrollViewComponentView and return false if contentInsetAdjustmentBehavior is automatic;

I didn't want to introduce any changes to the behavior of the components in this PR so it can be safely merged without much testing. This change can be probably made in some following PR if it is a proper solution.

WoLewicki avatar Nov 21 '22 13:11 WoLewicki

@sammy-SC can you explain why you wouldn't want this escape hatch, please? What are the downsides from the React Native perspective?

grahammendick avatar Nov 21 '22 13:11 grahammendick

@sammy-SC can you explain why you wouldn't want this escape hatch, please? What are the downsides from the React Native perspective?

It is not that I do not want it. If it makes it easier to implement components, I'm all up for it.

I'm just being cautious to and trying to explore other options we have before merging this PR. This API is used by 3rd party components and it is a contract React Native has with components. Therefore, we need to think twice before making any change as it is difficult to undo in the future. I have a couple ideas for optimisations for the new architecture but they depend on recycling working well.

One escape hatch recycling has is to just wipe out state of view component inside prepareForRecycle. But as @WoLewicki mentioned, it did not work for him. I just would like to learn more about the use case he had and why clearing all state in prepareForRecycle did not work for him.

sammy-SC avatar Nov 21 '22 14:11 sammy-SC

I've just checked our example app on Fabric, and I can still see one scenario where our logic does not work, being calling navigation.replace() method. Due to view recycling, the same view that has been just unmounted with the previous VC, is now attached with the same VC (since we do not destroy VCs on recycling, because it would mess with the native transitions). The native platform code does not detect the change in attached VCs then so it does not perform any screen transition, leading to wrong application state. I don't remember any other issues that were not solved yet, but this one is pretty big.

WoLewicki avatar Nov 21 '22 15:11 WoLewicki

That sounds like a 'bug' with react navigation because the component shouldn't unmount if it's still used by the transition. The Navigation router waits until the transition is complete before unmounting the component. I'm still supportive of the PR

grahammendick avatar Nov 21 '22 15:11 grahammendick

We made native-stack subscribe to the unmount on purpose to make the JS code as simple as possible 🤷‍♂️

WoLewicki avatar Nov 21 '22 17:11 WoLewicki

@sammy-SC has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

facebook-github-bot avatar Feb 17 '23 10:02 facebook-github-bot

@sammy-SC is this PR going to be added to the core? Should I do something about it ?

WoLewicki avatar Jul 06 '23 09:07 WoLewicki

@WoLewicki I'm on board with having a way to opt out of view recycling. You have made some compelling arguments why this is useful and I think it would make writing native components easier.

I don't think it should live on ComponentView itself though. This gives impression that each component instance can choose whether it is recyclable or not. But this isn't the case. It is always type of component that is recycle, not individual instances. Therefore method shouldBeRecycled should be probably a static method and its value stored in RCTComponentViewClassDescriptor.

sammy-SC avatar Jul 19 '23 09:07 sammy-SC

@sammy-SC I moved it to RCTComponentViewClassDescriptor as you suggested, but left the check for selector only, so the value returned by it is not taken into account. Is it a problem? If someone wants to subscribe to it, I think it should mean that he wants to disable this behavior.

WoLewicki avatar Jul 20 '23 17:07 WoLewicki

Ok I pushed a better (hopefully) version that checks it correctly. Could you check it again @sammy-SC ?

WoLewicki avatar Oct 09 '23 16:10 WoLewicki

@WoLewicki can I ask you to rebase on top of main? I think that this diff is pointing to a very old commit of RN.

cipolleschi avatar Jan 04 '24 16:01 cipolleschi

@cipolleschi should be the newest main now.

WoLewicki avatar Jan 04 '24 16:01 WoLewicki

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

facebook-github-bot avatar Jan 04 '24 17:01 facebook-github-bot

@cipolleschi merged this pull request in facebook/react-native@613a5a75977d84333df7cbd5701e01a7ab5a3385.

facebook-github-bot avatar Jan 05 '24 15:01 facebook-github-bot