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
+});