From d6c8f189e77e684785f317d5a4abfbb2be57e489 Mon Sep 17 00:00:00 2001 From: empyrical Date: Wed, 17 Oct 2018 21:58:15 -0700 Subject: [PATCH] Introduce 'setAndForwardRef' helper function (#21823) Summary: This PR introduces a new helper function called `setAndForwardRef`. It is intended to help with moving components that depend on `NativeMethodsMixin` off of `createReactClass`. It allows for classes that depend on having a ref to a native component to be able to also forward the native component ref to user code. Usage is like this: ```js class MyView extends React.Component { _nativeRef = null; _setNativeRef = setAndForwardRef({ getForwardedRef: () => this.props.forwardedRef, setLocalRef: ref => { this._nativeRef = ref; }, }); render() { return ; } } const MyViewWithRef = React.forwardRef((props, ref) => ( )); module.exports = MyViewWithRef; ``` Pull Request resolved: https://github.com/facebook/react-native/pull/21823 Differential Revision: D10436673 Pulled By: TheSavior fbshipit-source-id: 32e167bb3ea3234f08d5715168b0e61e4e035a7c --- .../__tests__/setAndForwardRef-test.js | 134 ++++++++++++++++++ Libraries/Utilities/setAndForwardRef.js | 70 +++++++++ 2 files changed, 204 insertions(+) create mode 100644 Libraries/Utilities/__tests__/setAndForwardRef-test.js create mode 100644 Libraries/Utilities/setAndForwardRef.js diff --git a/Libraries/Utilities/__tests__/setAndForwardRef-test.js b/Libraries/Utilities/__tests__/setAndForwardRef-test.js new file mode 100644 index 00000000000..e1899bb3c2f --- /dev/null +++ b/Libraries/Utilities/__tests__/setAndForwardRef-test.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @emails oncall+react_native + */ + +'use strict'; + +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); + +const setAndForwardRef = require('setAndForwardRef'); + +describe('setAndForwardRef', () => { + let innerFuncCalled = false; + let outerFuncCalled = false; + + class ForwardedComponent extends React.Component<{||}> { + testFunc() { + innerFuncCalled = true; + return true; + } + + render() { + return null; + } + } + + type Props = $ReadOnly<{| + callFunc?: ?boolean, + forwardedRef: React.Ref, + |}>; + + class TestComponent extends React.Component { + _nativeRef: ?React.ElementRef = null; + _setNativeRef = setAndForwardRef({ + getForwardedRef: () => this.props.forwardedRef, + setLocalRef: ref => { + this._nativeRef = ref; + }, + }); + + componentDidMount() { + if (this.props.callFunc) { + outerFuncCalled = this._nativeRef && this._nativeRef.testFunc(); + } + } + + render() { + return ; + } + } + + // $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet. + const TestComponentWithRef = React.forwardRef((props, ref) => ( + + )); + + beforeEach(() => { + innerFuncCalled = false; + outerFuncCalled = false; + }); + + it('should forward refs (function-based)', () => { + let testRef: ?React.ElementRef = null; + + ReactTestRenderer.create( + { + testRef = ref; + }} + />, + ); + + const val = testRef && testRef.testFunc(); + + expect(innerFuncCalled).toBe(true); + expect(val).toBe(true); + }); + + it('should forward refs (createRef-based)', () => { + const createdRef = React.createRef(); + + ReactTestRenderer.create(); + + const val = createdRef.current && createdRef.current.testFunc(); + + expect(innerFuncCalled).toBe(true); + expect(val).toBe(true); + }); + + it('should forward refs (string-based)', () => { + class Test extends React.Component<{||}> { + refs: $ReadOnly<{| + stringRef?: ?React.ElementRef, + |}>; + + componentDidMount() { + /* eslint-disable react/no-string-refs */ + this.refs.stringRef && this.refs.stringRef.testFunc(); + /* eslint-enable react/no-string-refs */ + } + + render() { + /** + * Can't directly pass the test component to `ReactTestRenderer.create`, + * otherwise it will throw. See: + * https://reactjs.org/warnings/refs-must-have-owner.html#strings-refs-outside-the-render-method + */ + /* eslint-disable react/no-string-refs */ + return ; + /* eslint-enable react/no-string-refs */ + } + } + + ReactTestRenderer.create(); + + expect(innerFuncCalled).toBe(true); + }); + + it('should be able to use the ref from inside of the forwarding class', () => { + expect(() => + ReactTestRenderer.create(), + ).not.toThrow(); + + expect(innerFuncCalled).toBe(true); + expect(outerFuncCalled).toBe(true); + }); +}); diff --git a/Libraries/Utilities/setAndForwardRef.js b/Libraries/Utilities/setAndForwardRef.js new file mode 100644 index 00000000000..c135e73788f --- /dev/null +++ b/Libraries/Utilities/setAndForwardRef.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const invariant = require('fbjs/lib/invariant'); + +import type React from 'React'; + +type Args = $ReadOnly<{| + getForwardedRef: () => ?React.Ref, + setLocalRef: (ref: React.ElementRef) => mixed, +|}>; + +/** + * This is a helper function for when a component needs to be able to forward a ref + * to a child component, but still needs to have access to that component as part of + * its implementation. + * + * Its main use case is in wrappers for native components. + * + * Usage: + * + * class MyView extends React.Component { + * _nativeRef = null; + * + * _setNativeRef = setAndForwardRef({ + * getForwardedRef: () => this.props.forwardedRef, + * setLocalRef: ref => { + * this._nativeRef = ref; + * }, + * }); + * + * render() { + * return ; + * } + * } + * + * const MyViewWithRef = React.forwardRef((props, ref) => ( + * + * )); + * + * module.exports = MyViewWithRef; + */ + +function setAndForwardRef({getForwardedRef, setLocalRef}: Args) { + return function forwardRef(ref: React.ElementRef) { + const forwardedRef = getForwardedRef(); + + setLocalRef(ref); + + // Forward to user ref prop (if one has been specified) + if (typeof forwardedRef === 'function') { + // Handle function-based refs. String-based refs are handled as functions. + forwardedRef(ref); + } else if (typeof forwardedRef === 'object' && forwardedRef != null) { + // Handle createRef-based refs + forwardedRef.current = ref; + } + }; +} + +module.exports = setAndForwardRef;