-
Notifications
You must be signed in to change notification settings - Fork 135
Description
Context
React Native has native components like RCTView, RCTText, RCTImage, etc that are implemented in native code. When JS code tries to render them, it is done by rendering a string:
const RCTView = 'RCTView';
return <RCTView {...props} />;
React knows that components that are strings are special native components, vs functions or classes which are JS components. When these special native components are rendered, React creates "host" components in JS that add some additional behavior and metadata. This includes methods like blur
, focus
, measure
, measureInWindow
, measureLayout
, and setNativeProps
.
Problems
Every time a native component is rendered, React has to construct a wrapper class for this node that contains these methods because React doesn't know if these methods will be accessed on a ref of these components.
Creating these classes results in additional memory usage and runtime perf overhead. There are also a couple of different situations where these methods can be created which leads to a bunch of similar code, bloating the default React Native bundle. Some examples include NativeMethodsMixin
, ReactFabricHostConfig.js
, ReactNativeComponent.js
, and ReactNativeFiberHostComponent.js
.
Furthermore, there can be complexities when creating JS components that render to these native components, wanting to provide custom imperative methods while also providing access to the native component methods. For example, native components have focus
and blur
methods, but if the JS TextInput component wants to expose a clear
method, it isn't able to forward the ref to the native component because that wouldn't include clear
, and it can't not use forwardRef
because then it wouldn't include focus
and blur
. This has led us to try to duplicate all the native methods in the JS component as passthrough methods to the underlying ref. Another common approach to this problem is by having an imperative method like getNativeRef()
. This approach also isn't great because it leaks implementation details and is prone to breakages when refactoring. Definitely not ideal.
Proposal
Instead of having to create a wrapper class for every native component just in case one of these methods is called, we propose moving these methods to be an export from ReactNative
. That means instead of calling the methods on the ref, you would pass the ref to the publicly exported functions.
Concretely, this would be an api change from calling setNativeProps like this:
refToNativeComponent.setNativeProps({text: ''});
to
import {setNativeProps} from 'react-native';
// ...
setNativeProps(refToNativeComponent, {text: ''});
The implementation of these methods would still be defined in the ReactNativeRenderer which is required because the implementation for Fabric is different than the current one for Paper. They would be exported from react-native's public API as a re-export from the renderer. This is currently what we do to export findNodeHandle
Thoughts on Implementation
I think we can make this migration incrementally by first creating the new versions of these methods that take a ref as an argument. We would want this implementation to be the source of truth for the methods instead of having these just call the method on the ref.
We can then change the NativeMethodsMixin
and HostComponents
to call the top level API. This also gives us a layer at which to add a deprecation warning to ask out of repo callsites to migrate.
Once we decide to finally remove the old implementation we can clean up the renderers removing the old approach and realize the performance and memory wins.
cc @sebmarkbage