@samitouri / QOS-React / commits / 83f76e4db9

ForwardRefs supports propTypes (#12911)

* Moved some internal forwardRef tests to not be internal * ForwardRef supports propTypes

Brian Vaughn committed May 29, 2018 at 09:50 UTC 83f76e4db9a060911774f497b5715bd4d4f42a9c
3 files changed +194 -103
packages/react/src/ReactElementValidator.js
+36 -13
@@ -16,7 +16,11 @@ import lowPriorityWarning from 'shared/lowPriorityWarning';
16 import describeComponentFrame from 'shared/describeComponentFrame';
17 import isValidElementType from 'shared/isValidElementType';
18 import getComponentName from 'shared/getComponentName';
19 -import {getIteratorFn, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
19 +import {
20 + getIteratorFn,
21 + REACT_FORWARD_REF_TYPE,
22 + REACT_FRAGMENT_TYPE,
23 +} from 'shared/ReactSymbols';
24 import checkPropTypes from 'prop-types/checkPropTypes';
25 import warning from 'fbjs/lib/warning';
26
@@ -42,10 +46,20 @@ if (__DEV__) {
46 return '#text';
47 } else if (typeof element.type === 'string') {
48 return element.type;
45 - } else if (element.type === REACT_FRAGMENT_TYPE) {
49 + }
50 +
51 + const type = element.type;
52 + if (type === REACT_FRAGMENT_TYPE) {
53 return 'React.Fragment';
54 + } else if (
55 + typeof type === 'object' &&
56 + type !== null &&
57 + type.$$typeof === REACT_FORWARD_REF_TYPE
58 + ) {
59 + const functionName = type.render.displayName || type.render.name || '';
60 + return functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef';
61 } else {
48 - return element.type.displayName || element.type.name || 'Unknown';
62 + return type.displayName || type.name || 'Unknown';
63 }
64 };
65
@@ -213,20 +227,29 @@ function validateChildKeys(node, parentType) {
227 * @param {ReactElement} element
228 */
229 function validatePropTypes(element) {
216 - const componentClass = element.type;
217 - if (typeof componentClass !== 'function') {
230 + const type = element.type;
231 + let name, propTypes;
232 + if (typeof type === 'function') {
233 + // Class or functional component
234 + name = type.displayName || type.name;
235 + propTypes = type.propTypes;
236 + } else if (
237 + typeof type === 'object' &&
238 + type !== null &&
239 + type.$$typeof === REACT_FORWARD_REF_TYPE
240 + ) {
241 + // ForwardRef
242 + const functionName = type.render.displayName || type.render.name || '';
243 + name = functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef';
244 + propTypes = type.propTypes;
245 + } else {
246 return;
247 }
220 - const name = componentClass.displayName || componentClass.name;
221 - const propTypes = componentClass.propTypes;
248 if (propTypes) {
249 currentlyValidatingElement = element;
250 checkPropTypes(propTypes, element.props, 'prop', name, getStackAddendum);
251 currentlyValidatingElement = null;
226 - } else if (
227 - componentClass.PropTypes !== undefined &&
228 - !propTypesMisspellWarningShown
229 - ) {
252 + } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {
253 propTypesMisspellWarningShown = true;
254 warning(
255 false,
@@ -234,9 +257,9 @@ function validatePropTypes(element) {
257 name || 'Unknown',
258 );
259 }
237 - if (typeof componentClass.getDefaultProps === 'function') {
260 + if (typeof type.getDefaultProps === 'function') {
261 warning(
239 - componentClass.getDefaultProps.isReactClassApproved,
262 + type.getDefaultProps.isReactClassApproved,
263 'getDefaultProps is only used on classic React.createClass ' +
264 'definitions. Use a static property named `defaultProps` instead.',
265 );
packages/react/src/__tests__/forwardRef-test.internal.js
-90
@@ -94,31 +94,6 @@ describe('forwardRef', () => {
94 expect(ref.current instanceof Child).toBe(true);
95 });
96
97 - it('should update refs when switching between children', () => {
98 - function FunctionalComponent({forwardedRef, setRefOnDiv}) {
99 - return (
100 - <section>
101 - <div ref={setRefOnDiv ? forwardedRef : null}>First</div>
102 - <span ref={setRefOnDiv ? null : forwardedRef}>Second</span>
103 - </section>
104 - );
105 - }
106 -
107 - const RefForwardingComponent = React.forwardRef((props, ref) => (
108 - <FunctionalComponent {...props} forwardedRef={ref} />
109 - ));
110 -
111 - const ref = React.createRef();
112 -
113 - ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={true} />);
114 - ReactNoop.flush();
115 - expect(ref.current.type).toBe('div');
116 -
117 - ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={false} />);
118 - ReactNoop.flush();
119 - expect(ref.current.type).toBe('span');
120 - });
121 -
97 it('should maintain child instance and ref through updates', () => {
98 class Child extends React.Component {
99 constructor(props) {
@@ -206,32 +181,6 @@ describe('forwardRef', () => {
181 expect(ref.current).toBe(null);
182 });
183
209 - it('should support rendering null', () => {
210 - const RefForwardingComponent = React.forwardRef((props, ref) => null);
211 -
212 - const ref = React.createRef();
213 -
214 - ReactNoop.render(<RefForwardingComponent ref={ref} />);
215 - ReactNoop.flush();
216 - expect(ref.current).toBe(null);
217 - });
218 -
219 - it('should support rendering null for multiple children', () => {
220 - const RefForwardingComponent = React.forwardRef((props, ref) => null);
221 -
222 - const ref = React.createRef();
223 -
224 - ReactNoop.render(
225 - <div>
226 - <div />
227 - <RefForwardingComponent ref={ref} />
228 - <div />
229 - </div>,
230 - );
231 - ReactNoop.flush();
232 - expect(ref.current).toBe(null);
233 - });
234 -
184 it('should not re-run the render callback on a deep setState', () => {
185 let inst;
186
@@ -264,43 +213,4 @@ describe('forwardRef', () => {
213 inst.setState({});
214 expect(ReactNoop.flush()).toEqual(['Inner']);
215 });
267 -
268 - it('should warn if not provided a callback during creation', () => {
269 - expect(() => React.forwardRef(undefined)).toWarnDev(
270 - 'forwardRef requires a render function but was given undefined.',
271 - );
272 - expect(() => React.forwardRef(null)).toWarnDev(
273 - 'forwardRef requires a render function but was given null.',
274 - );
275 - expect(() => React.forwardRef('foo')).toWarnDev(
276 - 'forwardRef requires a render function but was given string.',
277 - );
278 - });
279 -
280 - it('should warn if no render function is provided', () => {
281 - expect(React.forwardRef).toWarnDev(
282 - 'forwardRef requires a render function but was given undefined.',
283 - );
284 - });
285 -
286 - it('should warn if the render function provided has propTypes or defaultProps attributes', () => {
287 - function renderWithPropTypes() {
288 - return null;
289 - }
290 - renderWithPropTypes.propTypes = {};
291 -
292 - function renderWithDefaultProps() {
293 - return null;
294 - }
295 - renderWithDefaultProps.defaultProps = {};
296 -
297 - expect(() => React.forwardRef(renderWithPropTypes)).toWarnDev(
298 - 'forwardRef render functions do not support propTypes or defaultProps. ' +
299 - 'Did you accidentally pass a React component?',
300 - );
301 - expect(() => React.forwardRef(renderWithDefaultProps)).toWarnDev(
302 - 'forwardRef render functions do not support propTypes or defaultProps. ' +
303 - 'Did you accidentally pass a React component?',
304 - );
305 - });
216 });
packages/react/src/__tests__/forwardRef-test.js new
+158
@@ -0,0 +1,158 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +describe('forwardRef', () => {
13 + let PropTypes;
14 + let React;
15 + let ReactNoop;
16 +
17 + beforeEach(() => {
18 + jest.resetModules();
19 + PropTypes = require('prop-types');
20 + React = require('react');
21 + ReactNoop = require('react-noop-renderer');
22 + });
23 +
24 + it('should update refs when switching between children', () => {
25 + function FunctionalComponent({forwardedRef, setRefOnDiv}) {
26 + return (
27 + <section>
28 + <div ref={setRefOnDiv ? forwardedRef : null}>First</div>
29 + <span ref={setRefOnDiv ? null : forwardedRef}>Second</span>
30 + </section>
31 + );
32 + }
33 +
34 + const RefForwardingComponent = React.forwardRef((props, ref) => (
35 + <FunctionalComponent {...props} forwardedRef={ref} />
36 + ));
37 +
38 + const ref = React.createRef();
39 +
40 + ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={true} />);
41 + ReactNoop.flush();
42 + expect(ref.current.type).toBe('div');
43 +
44 + ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={false} />);
45 + ReactNoop.flush();
46 + expect(ref.current.type).toBe('span');
47 + });
48 +
49 + it('should support rendering null', () => {
50 + const RefForwardingComponent = React.forwardRef((props, ref) => null);
51 +
52 + const ref = React.createRef();
53 +
54 + ReactNoop.render(<RefForwardingComponent ref={ref} />);
55 + ReactNoop.flush();
56 + expect(ref.current).toBe(null);
57 + });
58 +
59 + it('should support rendering null for multiple children', () => {
60 + const RefForwardingComponent = React.forwardRef((props, ref) => null);
61 +
62 + const ref = React.createRef();
63 +
64 + ReactNoop.render(
65 + <div>
66 + <div />
67 + <RefForwardingComponent ref={ref} />
68 + <div />
69 + </div>,
70 + );
71 + ReactNoop.flush();
72 + expect(ref.current).toBe(null);
73 + });
74 +
75 + it('should support propTypes and defaultProps', () => {
76 + function FunctionalComponent({forwardedRef, optional, required}) {
77 + return (
78 + <div ref={forwardedRef}>
79 + {optional}
80 + {required}
81 + </div>
82 + );
83 + }
84 +
85 + const RefForwardingComponent = React.forwardRef(function NamedFunction(
86 + props,
87 + ref,
88 + ) {
89 + return <FunctionalComponent {...props} forwardedRef={ref} />;
90 + });
91 + RefForwardingComponent.propTypes = {
92 + optional: PropTypes.string,
93 + required: PropTypes.string.isRequired,
94 + };
95 + RefForwardingComponent.defaultProps = {
96 + optional: 'default',
97 + };
98 +
99 + const ref = React.createRef();
100 +
101 + ReactNoop.render(
102 + <RefForwardingComponent ref={ref} optional="foo" required="bar" />,
103 + );
104 + ReactNoop.flush();
105 + expect(ref.current.children).toEqual([{text: 'foo'}, {text: 'bar'}]);
106 +
107 + ReactNoop.render(<RefForwardingComponent ref={ref} required="foo" />);
108 + ReactNoop.flush();
109 + expect(ref.current.children).toEqual([{text: 'default'}, {text: 'foo'}]);
110 +
111 + expect(() =>
112 + ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
113 + ).toWarnDev(
114 + 'Warning: Failed prop type: The prop `required` is marked as required in ' +
115 + '`ForwardRef(NamedFunction)`, but its value is `undefined`.\n' +
116 + ' in ForwardRef(NamedFunction) (at **)',
117 + );
118 + });
119 +
120 + it('should warn if not provided a callback during creation', () => {
121 + expect(() => React.forwardRef(undefined)).toWarnDev(
122 + 'forwardRef requires a render function but was given undefined.',
123 + );
124 + expect(() => React.forwardRef(null)).toWarnDev(
125 + 'forwardRef requires a render function but was given null.',
126 + );
127 + expect(() => React.forwardRef('foo')).toWarnDev(
128 + 'forwardRef requires a render function but was given string.',
129 + );
130 + });
131 +
132 + it('should warn if no render function is provided', () => {
133 + expect(React.forwardRef).toWarnDev(
134 + 'forwardRef requires a render function but was given undefined.',
135 + );
136 + });
137 +
138 + it('should warn if the render function provided has propTypes or defaultProps attributes', () => {
139 + function renderWithPropTypes() {
140 + return null;
141 + }
142 + renderWithPropTypes.propTypes = {};
143 +
144 + function renderWithDefaultProps() {
145 + return null;
146 + }
147 + renderWithDefaultProps.defaultProps = {};
148 +
149 + expect(() => React.forwardRef(renderWithPropTypes)).toWarnDev(
150 + 'forwardRef render functions do not support propTypes or defaultProps. ' +
151 + 'Did you accidentally pass a React component?',
152 + );
153 + expect(() => React.forwardRef(renderWithDefaultProps)).toWarnDev(
154 + 'forwardRef render functions do not support propTypes or defaultProps. ' +
155 + 'Did you accidentally pass a React component?',
156 + );
157 + });
158 +});