diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 2c033324a6f52..12770d5122d2b 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactFabricType} from './ReactNativeTypes'; +import type {ReactFabricType, HostComponent} from './ReactNativeTypes'; import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactFabricInjection'; @@ -44,6 +44,54 @@ import warningWithoutStack from 'shared/warningWithoutStack'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; +function findHostInstance_deprecated( + componentOrHandle: any, +): ?HostComponent { + if (__DEV__) { + const owner = ReactCurrentOwner.current; + if (owner !== null && owner.stateNode !== null) { + warningWithoutStack( + owner.stateNode._warnedAboutRefsInRender, + '%s is accessing findNodeHandle inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + getComponentName(owner.type) || 'A component', + ); + + owner.stateNode._warnedAboutRefsInRender = true; + } + } + if (componentOrHandle == null) { + return null; + } + if (componentOrHandle._nativeTag) { + return componentOrHandle; + } + if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { + return componentOrHandle.canonical; + } + let hostInstance; + if (__DEV__) { + hostInstance = findHostInstanceWithWarning( + componentOrHandle, + 'findHostInstance_deprecated', + ); + } else { + hostInstance = findHostInstance(componentOrHandle); + } + + if (hostInstance == null) { + return hostInstance; + } + if ((hostInstance: any).canonical) { + // Fabric + return (hostInstance: any).canonical; + } + return hostInstance; +} + function findNodeHandle(componentOrHandle: any): ?number { if (__DEV__) { const owner = ReactCurrentOwner.current; @@ -108,6 +156,9 @@ const roots = new Map(); const ReactFabric: ReactFabricType = { NativeComponent: ReactNativeComponent(findNodeHandle, findHostInstance), + // This is needed for implementation details of TouchableNativeFeedback + // Remove this once TouchableNativeFeedback doesn't use cloneElement + findHostInstance_deprecated, findNodeHandle, dispatchCommand(handle: any, command: string, args: Array) { diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 8cc9bcd0361e1..d0aad7b2a2253 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -7,7 +7,7 @@ * @flow */ -import type {ReactNativeType} from './ReactNativeTypes'; +import type {ReactNativeType, HostComponent} from './ReactNativeTypes'; import type {ReactNodeList} from 'shared/ReactTypes'; import './ReactNativeInjection'; @@ -47,6 +47,54 @@ import warningWithoutStack from 'shared/warningWithoutStack'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; +function findHostInstance_deprecated( + componentOrHandle: any, +): ?HostComponent { + if (__DEV__) { + const owner = ReactCurrentOwner.current; + if (owner !== null && owner.stateNode !== null) { + warningWithoutStack( + owner.stateNode._warnedAboutRefsInRender, + '%s is accessing findNodeHandle inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + getComponentName(owner.type) || 'A component', + ); + + owner.stateNode._warnedAboutRefsInRender = true; + } + } + if (componentOrHandle == null) { + return null; + } + if (componentOrHandle._nativeTag) { + return componentOrHandle; + } + if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { + return componentOrHandle.canonical; + } + let hostInstance; + if (__DEV__) { + hostInstance = findHostInstanceWithWarning( + componentOrHandle, + 'findHostInstance_deprecated', + ); + } else { + hostInstance = findHostInstance(componentOrHandle); + } + + if (hostInstance == null) { + return hostInstance; + } + if ((hostInstance: any).canonical) { + // Fabric + return (hostInstance: any).canonical; + } + return hostInstance; +} + function findNodeHandle(componentOrHandle: any): ?number { if (__DEV__) { const owner = ReactCurrentOwner.current; @@ -117,6 +165,9 @@ const roots = new Map(); const ReactNativeRenderer: ReactNativeType = { NativeComponent: ReactNativeComponent(findNodeHandle, findHostInstance), + // This is needed for implementation details of TouchableNativeFeedback + // Remove this once TouchableNativeFeedback doesn't use cloneElement + findHostInstance_deprecated, findNodeHandle, dispatchCommand(handle: any, command: string, args: Array) { diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 8474f0866d775..df67d953f749c 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -779,6 +779,81 @@ describe('ReactFabric', () => { expect(touchStart2).toBeCalled(); }); + it('findHostInstance_deprecated should warn if used to find a host component inside StrictMode', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let parent = undefined; + let child = undefined; + + class ContainsStrictModeChild extends React.Component { + render() { + return ( + + (child = n)} /> + + ); + } + } + + ReactFabric.render( (parent = n)} />, 11); + + let match; + expect( + () => (match = ReactFabric.findHostInstance_deprecated(parent)), + ).toWarnDev([ + 'Warning: findHostInstance_deprecated is deprecated in StrictMode. ' + + 'findHostInstance_deprecated was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node' + + '\n in RCTView (at **)' + + '\n in StrictMode (at **)' + + '\n in ContainsStrictModeChild (at **)', + ]); + expect(match).toBe(child); + }); + + it('findHostInstance_deprecated should warn if passed a component that is inside StrictMode', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let parent = undefined; + let child = undefined; + + class IsInStrictMode extends React.Component { + render() { + return (child = n)} />; + } + } + + ReactFabric.render( + + (parent = n)} /> + , + 11, + ); + + let match; + expect( + () => (match = ReactFabric.findHostInstance_deprecated(parent)), + ).toWarnDev([ + 'Warning: findHostInstance_deprecated is deprecated in StrictMode. ' + + 'findHostInstance_deprecated was passed an instance of IsInStrictMode which is inside StrictMode. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node' + + '\n in RCTView (at **)' + + '\n in IsInStrictMode (at **)' + + '\n in StrictMode (at **)', + ]); + expect(match).toBe(child); + }); + it('findNodeHandle should warn if used to find a host component inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index ee63a76e92aa2..71a6080b377a4 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -34,6 +34,26 @@ describe('created with ReactFabric called with ReactNative', () => { .ReactNativeViewConfigRegistry.register; }); + it('find Fabric instances with the RN renderer', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {title: true}, + uiViewClassName: 'RCTView', + })); + + let ref = React.createRef(); + + class Component extends React.Component { + render() { + return ; + } + } + + ReactFabric.render(, 11); + + let instance = ReactNative.findHostInstance_deprecated(ref.current); + expect(instance._nativeTag).toBe(2); + }); + it('find Fabric nodes with the RN renderer', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, @@ -94,6 +114,26 @@ describe('created with ReactNative called with ReactFabric', () => { .ReactNativeViewConfigRegistry.register; }); + it('find Paper instances with the Fabric renderer', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {title: true}, + uiViewClassName: 'RCTView', + })); + + let ref = React.createRef(); + + class Component extends React.Component { + render() { + return ; + } + } + + ReactNative.render(, 11); + + let instance = ReactFabric.findHostInstance_deprecated(ref.current); + expect(instance._nativeTag).toBe(3); + }); + it('find Paper nodes with the Fabric renderer', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 74b2700f80791..ea41cf23af8b9 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -541,6 +541,81 @@ describe('ReactNative', () => { ); }); + it('findHostInstance_deprecated should warn if used to find a host component inside StrictMode', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let parent = undefined; + let child = undefined; + + class ContainsStrictModeChild extends React.Component { + render() { + return ( + + (child = n)} /> + + ); + } + } + + ReactNative.render( (parent = n)} />, 11); + + let match; + expect( + () => (match = ReactNative.findHostInstance_deprecated(parent)), + ).toWarnDev([ + 'Warning: findHostInstance_deprecated is deprecated in StrictMode. ' + + 'findHostInstance_deprecated was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node' + + '\n in RCTView (at **)' + + '\n in StrictMode (at **)' + + '\n in ContainsStrictModeChild (at **)', + ]); + expect(match).toBe(child); + }); + + it('findHostInstance_deprecated should warn if passed a component that is inside StrictMode', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let parent = undefined; + let child = undefined; + + class IsInStrictMode extends React.Component { + render() { + return (child = n)} />; + } + } + + ReactNative.render( + + (parent = n)} /> + , + 11, + ); + + let match; + expect( + () => (match = ReactNative.findHostInstance_deprecated(parent)), + ).toWarnDev([ + 'Warning: findHostInstance_deprecated is deprecated in StrictMode. ' + + 'findHostInstance_deprecated was passed an instance of IsInStrictMode which is inside StrictMode. ' + + 'Instead, add a ref directly to the element you want to reference. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-find-node' + + '\n in RCTView (at **)' + + '\n in IsInStrictMode (at **)' + + '\n in StrictMode (at **)', + ]); + expect(match).toBe(child); + }); + it('findNodeHandle should warn if used to find a host component inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true},