Skip to content

Commit 83f76e4

Browse files
authored
ForwardRefs supports propTypes (#12911)
* Moved some internal forwardRef tests to not be internal * ForwardRef supports propTypes
1 parent 4f1f909 commit 83f76e4

File tree

3 files changed

+194
-103
lines changed

3 files changed

+194
-103
lines changed

packages/react/src/ReactElementValidator.js

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import lowPriorityWarning from 'shared/lowPriorityWarning';
1616
import describeComponentFrame from 'shared/describeComponentFrame';
1717
import isValidElementType from 'shared/isValidElementType';
1818
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';
2024
import checkPropTypes from 'prop-types/checkPropTypes';
2125
import warning from 'fbjs/lib/warning';
2226

@@ -42,10 +46,20 @@ if (__DEV__) {
4246
return '#text';
4347
} else if (typeof element.type === 'string') {
4448
return element.type;
45-
} else if (element.type === REACT_FRAGMENT_TYPE) {
49+
}
50+
51+
const type = element.type;
52+
if (type === REACT_FRAGMENT_TYPE) {
4653
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';
4761
} else {
48-
return element.type.displayName || element.type.name || 'Unknown';
62+
return type.displayName || type.name || 'Unknown';
4963
}
5064
};
5165

@@ -213,30 +227,39 @@ function validateChildKeys(node, parentType) {
213227
* @param {ReactElement} element
214228
*/
215229
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 {
218246
return;
219247
}
220-
const name = componentClass.displayName || componentClass.name;
221-
const propTypes = componentClass.propTypes;
222248
if (propTypes) {
223249
currentlyValidatingElement = element;
224250
checkPropTypes(propTypes, element.props, 'prop', name, getStackAddendum);
225251
currentlyValidatingElement = null;
226-
} else if (
227-
componentClass.PropTypes !== undefined &&
228-
!propTypesMisspellWarningShown
229-
) {
252+
} else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {
230253
propTypesMisspellWarningShown = true;
231254
warning(
232255
false,
233256
'Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?',
234257
name || 'Unknown',
235258
);
236259
}
237-
if (typeof componentClass.getDefaultProps === 'function') {
260+
if (typeof type.getDefaultProps === 'function') {
238261
warning(
239-
componentClass.getDefaultProps.isReactClassApproved,
262+
type.getDefaultProps.isReactClassApproved,
240263
'getDefaultProps is only used on classic React.createClass ' +
241264
'definitions. Use a static property named `defaultProps` instead.',
242265
);

packages/react/src/__tests__/forwardRef-test.internal.js

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -94,31 +94,6 @@ describe('forwardRef', () => {
9494
expect(ref.current instanceof Child).toBe(true);
9595
});
9696

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-
12297
it('should maintain child instance and ref through updates', () => {
12398
class Child extends React.Component {
12499
constructor(props) {
@@ -206,32 +181,6 @@ describe('forwardRef', () => {
206181
expect(ref.current).toBe(null);
207182
});
208183

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-
235184
it('should not re-run the render callback on a deep setState', () => {
236185
let inst;
237186

@@ -264,43 +213,4 @@ describe('forwardRef', () => {
264213
inst.setState({});
265214
expect(ReactNoop.flush()).toEqual(['Inner']);
266215
});
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-
});
306216
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});

0 commit comments

Comments
 (0)