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

Introduce 'setAndForwardRef' helper function #21823

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
134 changes: 134 additions & 0 deletions Libraries/Utilities/__tests__/setAndForwardRef-test.js
Original file line number Diff line number Diff line change
@@ -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<typeof ForwardedComponent>,
|}>;

class TestComponent extends React.Component<Props> {
_nativeRef: ?React.ElementRef<typeof ForwardedComponent> = 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 <ForwardedComponent ref={this._setNativeRef} />;
}
}

// $FlowFixMe - TODO T29156721 `React.forwardRef` is not defined in Flow, yet.
const TestComponentWithRef = React.forwardRef((props, ref) => (
<TestComponent {...props} forwardedRef={ref} />
));

beforeEach(() => {
innerFuncCalled = false;
outerFuncCalled = false;
});

it('should forward refs (function-based)', () => {
let testRef: ?React.ElementRef<typeof ForwardedComponent> = null;

ReactTestRenderer.create(
<TestComponentWithRef
ref={ref => {
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<typeof ForwardedComponent>();

ReactTestRenderer.create(<TestComponentWithRef ref={createdRef} />);

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<typeof ForwardedComponent>,
|}>;

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 <TestComponentWithRef ref="stringRef" />;
/* eslint-enable react/no-string-refs */
}
}

ReactTestRenderer.create(<Test />);

expect(innerFuncCalled).toBe(true);
});

it('should be able to use the ref from inside of the forwarding class', () => {
expect(() =>
ReactTestRenderer.create(<TestComponentWithRef callFunc={true} />),
).not.toThrow();

expect(innerFuncCalled).toBe(true);
expect(outerFuncCalled).toBe(true);
});
empyrical marked this conversation as resolved.
Show resolved Hide resolved
empyrical marked this conversation as resolved.
Show resolved Hide resolved
});
70 changes: 70 additions & 0 deletions Libraries/Utilities/setAndForwardRef.js
Original file line number Diff line number Diff line change
@@ -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<any>,
setLocalRef: (ref: React.ElementRef<any>) => 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 <View ref={this._setNativeRef} />;
* }
* }
*
* const MyViewWithRef = React.forwardRef((props, ref) => (
* <MyView {...props} forwardedRef={ref} />
* ));
*
* module.exports = MyViewWithRef;
*/

function setAndForwardRef({getForwardedRef, setLocalRef}: Args) {
return function forwardRef(ref: React.ElementRef<any>) {
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;