RFC #30: React.forwardRef implementation (#12346)
Added React.forwardRef support to react-reconciler based renders and the SSR partial renderer.
Brian Vaughn committed
Mar 14, 2018 at 13:07 UTC
bc70441c8b3fa85338283af3eeb47b5d15e9dbfe
13 files changed
+368
-2
packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js
+21
@@ -96,4 +96,25 @@ describe('ReactDOMServerIntegration', () => {
96
expect(component.refs.myDiv).toBe(root.firstChild);
97
});
98
});
99
+
100
+ it('should forward refs', async () => {
101
+ const divRef = React.createRef();
102
+
103
+ class InnerComponent extends React.Component {
104
+ render() {
105
+ return <div ref={this.props.forwardedRef}>{this.props.value}</div>;
106
+ }
107
+ }
108
+
109
+ const OuterComponent = React.forwardRef((props, ref) => (
110
+ <InnerComponent {...props} forwardedRef={ref} />
111
+ ));
112
+
113
+ await clientRenderOnServerString(
114
+ <OuterComponent ref={divRef} value="hello" />,
115
+ );
116
+
117
+ expect(divRef.current).not.toBe(null);
118
+ expect(divRef.current.textContent).toBe('hello');
119
+ });
120
});
packages/react-dom/src/server/ReactPartialRenderer.js
+20
@@ -27,6 +27,7 @@ import describeComponentFrame from 'shared/describeComponentFrame';
27
import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
28
import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
29
import {
30
+ REACT_FORWARD_REF_TYPE,
31
REACT_FRAGMENT_TYPE,
32
REACT_STRICT_MODE_TYPE,
33
REACT_ASYNC_MODE_TYPE,
@@ -841,6 +842,25 @@ class ReactDOMServerRenderer {
842
}
843
if (typeof elementType === 'object' && elementType !== null) {
844
switch (elementType.$$typeof) {
845
+ case REACT_FORWARD_REF_TYPE: {
846
+ const element: ReactElement = ((nextChild: any): ReactElement);
847
+ const nextChildren = toArray(
848
+ elementType.render(element.props, element.ref),
849
+ );
850
+ const frame: Frame = {
851
+ type: null,
852
+ domNamespace: parentNamespace,
853
+ children: nextChildren,
854
+ childIndex: 0,
855
+ context: context,
856
+ footer: '',
857
+ };
858
+ if (__DEV__) {
859
+ ((frame: any): FrameDev).debugElementStack = [];
860
+ }
861
+ this.stack.push(frame);
862
+ return '';
863
+ }
864
case REACT_PROVIDER_TYPE: {
865
const provider: ReactProvider<any> = (nextChild: any);
866
const nextProps = provider.props;
packages/react-is/src/ReactIs.js
+6
@@ -13,6 +13,7 @@ import {
13
REACT_ASYNC_MODE_TYPE,
14
REACT_CONTEXT_TYPE,
15
REACT_ELEMENT_TYPE,
16
+ REACT_FORWARD_REF_TYPE,
17
REACT_FRAGMENT_TYPE,
18
REACT_PORTAL_TYPE,
19
REACT_PROVIDER_TYPE,
@@ -37,6 +38,7 @@ export function typeOf(object: any) {
38
39
switch ($$typeofType) {
40
case REACT_CONTEXT_TYPE:
41
+ case REACT_FORWARD_REF_TYPE:
42
case REACT_PROVIDER_TYPE:
43
return $$typeofType;
44
default:
@@ -55,6 +57,7 @@ export const AsyncMode = REACT_ASYNC_MODE_TYPE;
57
export const ContextConsumer = REACT_CONTEXT_TYPE;
58
export const ContextProvider = REACT_PROVIDER_TYPE;
59
export const Element = REACT_ELEMENT_TYPE;
60
+export const ForwardRef = REACT_FORWARD_REF_TYPE;
61
export const Fragment = REACT_FRAGMENT_TYPE;
62
export const Portal = REACT_PORTAL_TYPE;
63
export const StrictMode = REACT_STRICT_MODE_TYPE;
@@ -75,6 +78,9 @@ export function isElement(object: any) {
78
object.$$typeof === REACT_ELEMENT_TYPE
79
);
80
}
81
+export function isForwardRef(object: any) {
82
+ return typeOf(object) === REACT_FORWARD_REF_TYPE;
83
+}
84
export function isFragment(object: any) {
85
return typeOf(object) === REACT_FRAGMENT_TYPE;
86
}
packages/react-is/src/__tests__/ReactIs-test.js
+9
@@ -76,6 +76,15 @@ describe('ReactIs', () => {
76
expect(ReactIs.isElement(<React.StrictMode />)).toBe(true);
77
});
78
79
+ it('should identify ref forwarding component', () => {
80
+ const RefForwardingComponent = React.forwardRef((props, ref) => null);
81
+ expect(ReactIs.typeOf(<RefForwardingComponent />)).toBe(ReactIs.ForwardRef);
82
+ expect(ReactIs.isForwardRef(<RefForwardingComponent />)).toBe(true);
83
+ expect(ReactIs.isForwardRef({type: ReactIs.StrictMode})).toBe(false);
84
+ expect(ReactIs.isForwardRef(<React.unstable_AsyncMode />)).toBe(false);
85
+ expect(ReactIs.isForwardRef(<div />)).toBe(false);
86
+ });
87
+
88
it('should identify fragments', () => {
89
expect(ReactIs.typeOf(<React.Fragment />)).toBe(ReactIs.Fragment);
90
expect(ReactIs.isFragment(<React.Fragment />)).toBe(true);
packages/react-reconciler/src/ReactFiber.js
+5
@@ -25,6 +25,7 @@ import {
25
HostPortal,
26
CallComponent,
27
ReturnComponent,
28
+ ForwardRef,
29
Fragment,
30
Mode,
31
ContextProvider,
@@ -35,6 +36,7 @@ import getComponentName from 'shared/getComponentName';
36
import {NoWork} from './ReactFiberExpirationTime';
37
import {NoContext, AsyncMode, StrictMode} from './ReactTypeOfMode';
38
import {
39
+ REACT_FORWARD_REF_TYPE,
40
REACT_FRAGMENT_TYPE,
41
REACT_RETURN_TYPE,
42
REACT_CALL_TYPE,
@@ -357,6 +359,9 @@ export function createFiberFromElement(
359
// This is a consumer
360
fiberTag = ContextConsumer;
361
break;
362
+ case REACT_FORWARD_REF_TYPE:
363
+ fiberTag = ForwardRef;
364
+ break;
365
default:
366
if (typeof type.tag === 'number') {
367
// Currently assumed to be a continuation and therefore is a
packages/react-reconciler/src/ReactFiberBeginWork.js
+14
@@ -26,6 +26,7 @@ import {
26
CallComponent,
27
CallHandlerPhase,
28
ReturnComponent,
29
+ ForwardRef,
30
Fragment,
31
Mode,
32
ContextProvider,
@@ -153,6 +154,17 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
154
}
155
}
156
157
+ function updateForwardRef(current, workInProgress) {
158
+ const render = workInProgress.type.render;
159
+ const nextChildren = render(
160
+ workInProgress.pendingProps,
161
+ workInProgress.ref,
162
+ );
163
+ reconcileChildren(current, workInProgress, nextChildren);
164
+ memoizeProps(workInProgress, nextChildren);
165
+ return workInProgress.child;
166
+ }
167
+
168
function updateFragment(current, workInProgress) {
169
const nextChildren = workInProgress.pendingProps;
170
if (hasLegacyContextChanged()) {
@@ -1130,6 +1142,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1142
workInProgress,
1143
renderExpirationTime,
1144
);
1145
+ case ForwardRef:
1146
+ return updateForwardRef(current, workInProgress);
1147
case Fragment:
1148
return updateFragment(current, workInProgress);
1149
case Mode:
packages/react-reconciler/src/ReactFiberCompleteWork.js
+3
@@ -32,6 +32,7 @@ import {
32
ReturnComponent,
33
ContextProvider,
34
ContextConsumer,
35
+ ForwardRef,
36
Fragment,
37
Mode,
38
} from 'shared/ReactTypeOfWork';
@@ -603,6 +604,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
604
case ReturnComponent:
605
// Does nothing.
606
return null;
607
+ case ForwardRef:
608
+ return null;
609
case Fragment:
610
return null;
611
case Mode:
packages/react/src/React.js
+2
@@ -24,6 +24,7 @@ import {
24
isValidElement,
25
} from './ReactElement';
26
import {createContext} from './ReactContext';
27
+import forwardRef from './forwardRef';
28
import {
29
createElementWithValidation,
30
createFactoryWithValidation,
@@ -45,6 +46,7 @@ const React = {
46
PureComponent,
47
48
createContext,
49
+ forwardRef,
50
51
Fragment: REACT_FRAGMENT_TYPE,
52
StrictMode: REACT_STRICT_MODE_TYPE,
packages/react/src/ReactElementValidator.js
+3
-1
@@ -22,6 +22,7 @@ import {
22
REACT_ASYNC_MODE_TYPE,
23
REACT_PROVIDER_TYPE,
24
REACT_CONTEXT_TYPE,
25
+ REACT_FORWARD_REF_TYPE,
26
} from 'shared/ReactSymbols';
27
import checkPropTypes from 'prop-types/checkPropTypes';
28
import warning from 'fbjs/lib/warning';
@@ -297,7 +298,8 @@ export function createElementWithValidation(type, props, children) {
298
(typeof type === 'object' &&
299
type !== null &&
300
(type.$$typeof === REACT_PROVIDER_TYPE ||
300
- type.$$typeof === REACT_CONTEXT_TYPE));
301
+ type.$$typeof === REACT_CONTEXT_TYPE ||
302
+ type.$$typeof === REACT_FORWARD_REF_TYPE));
303
304
// We warn in this case but don't throw. We expect the element creation to
305
// succeed and there will likely be errors in render.
packages/react/src/__tests__/forwardRef-test.internal.js
new
+252
@@ -0,0 +1,252 @@
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 React;
14
+ let ReactFeatureFlags;
15
+ let ReactNoop;
16
+
17
+ beforeEach(() => {
18
+ jest.resetModules();
19
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
20
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
21
+ React = require('react');
22
+ ReactNoop = require('react-noop-renderer');
23
+ });
24
+
25
+ it('should work without a ref to be forwarded', () => {
26
+ class Child extends React.Component {
27
+ render() {
28
+ ReactNoop.yield(this.props.value);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function Wrapper(props) {
34
+ return <Child {...props} ref={props.forwardedRef} />;
35
+ }
36
+
37
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
38
+ <Wrapper {...props} forwardedRef={ref} />
39
+ ));
40
+
41
+ ReactNoop.render(<RefForwardingComponent value={123} />);
42
+ expect(ReactNoop.flush()).toEqual([123]);
43
+ });
44
+
45
+ it('should forward a ref for a single child', () => {
46
+ class Child extends React.Component {
47
+ render() {
48
+ ReactNoop.yield(this.props.value);
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function Wrapper(props) {
54
+ return <Child {...props} ref={props.forwardedRef} />;
55
+ }
56
+
57
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
58
+ <Wrapper {...props} forwardedRef={ref} />
59
+ ));
60
+
61
+ const ref = React.createRef();
62
+
63
+ ReactNoop.render(<RefForwardingComponent ref={ref} value={123} />);
64
+ expect(ReactNoop.flush()).toEqual([123]);
65
+ expect(ref.current instanceof Child).toBe(true);
66
+ });
67
+
68
+ it('should forward a ref for multiple children', () => {
69
+ class Child extends React.Component {
70
+ render() {
71
+ ReactNoop.yield(this.props.value);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function Wrapper(props) {
77
+ return <Child {...props} ref={props.forwardedRef} />;
78
+ }
79
+
80
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
81
+ <Wrapper {...props} forwardedRef={ref} />
82
+ ));
83
+
84
+ const ref = React.createRef();
85
+
86
+ ReactNoop.render(
87
+ <div>
88
+ <div />
89
+ <RefForwardingComponent ref={ref} value={123} />
90
+ <div />
91
+ </div>,
92
+ );
93
+ expect(ReactNoop.flush()).toEqual([123]);
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
+
122
+ it('should maintain child instance and ref through updates', () => {
123
+ class Child extends React.Component {
124
+ constructor(props) {
125
+ super(props);
126
+ }
127
+ render() {
128
+ ReactNoop.yield(this.props.value);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function Wrapper(props) {
134
+ return <Child {...props} ref={props.forwardedRef} />;
135
+ }
136
+
137
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
138
+ <Wrapper {...props} forwardedRef={ref} />
139
+ ));
140
+
141
+ let setRefCount = 0;
142
+ let ref;
143
+
144
+ const setRef = r => {
145
+ setRefCount++;
146
+ ref = r;
147
+ };
148
+
149
+ ReactNoop.render(<RefForwardingComponent ref={setRef} value={123} />);
150
+ expect(ReactNoop.flush()).toEqual([123]);
151
+ expect(ref instanceof Child).toBe(true);
152
+ expect(setRefCount).toBe(1);
153
+ ReactNoop.render(<RefForwardingComponent ref={setRef} value={456} />);
154
+ expect(ReactNoop.flush()).toEqual([456]);
155
+ expect(ref instanceof Child).toBe(true);
156
+ expect(setRefCount).toBe(1);
157
+ });
158
+
159
+ it('should not break lifecycle error handling', () => {
160
+ class ErrorBoundary extends React.Component {
161
+ state = {error: null};
162
+ componentDidCatch(error) {
163
+ ReactNoop.yield('ErrorBoundary.componentDidCatch');
164
+ this.setState({error});
165
+ }
166
+ render() {
167
+ if (this.state.error) {
168
+ ReactNoop.yield('ErrorBoundary.render: catch');
169
+ return null;
170
+ }
171
+ ReactNoop.yield('ErrorBoundary.render: try');
172
+ return this.props.children;
173
+ }
174
+ }
175
+
176
+ class BadRender extends React.Component {
177
+ render() {
178
+ ReactNoop.yield('BadRender throw');
179
+ throw new Error('oops!');
180
+ }
181
+ }
182
+
183
+ function Wrapper(props) {
184
+ ReactNoop.yield('Wrapper');
185
+ return <BadRender {...props} ref={props.forwardedRef} />;
186
+ }
187
+
188
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
189
+ <Wrapper {...props} forwardedRef={ref} />
190
+ ));
191
+
192
+ const ref = React.createRef();
193
+
194
+ ReactNoop.render(
195
+ <ErrorBoundary>
196
+ <RefForwardingComponent ref={ref} />
197
+ </ErrorBoundary>,
198
+ );
199
+ expect(ReactNoop.flush()).toEqual([
200
+ 'ErrorBoundary.render: try',
201
+ 'Wrapper',
202
+ 'BadRender throw',
203
+ 'ErrorBoundary.componentDidCatch',
204
+ 'ErrorBoundary.render: catch',
205
+ ]);
206
+ expect(ref.current).toBe(null);
207
+ });
208
+
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
+
235
+ it('should warn if not provided a callback during creation', () => {
236
+ expect(() => React.forwardRef(undefined)).toWarnDev(
237
+ 'forwardRef requires a render function but was given undefined.',
238
+ );
239
+ expect(() => React.forwardRef(null)).toWarnDev(
240
+ 'forwardRef requires a render function but was given null.',
241
+ );
242
+ expect(() => React.forwardRef('foo')).toWarnDev(
243
+ 'forwardRef requires a render function but was given string.',
244
+ );
245
+ });
246
+
247
+ it('should warn if no render function is provided', () => {
248
+ expect(React.forwardRef).toWarnDev(
249
+ 'forwardRef requires a render function but was given undefined.',
250
+ );
251
+ });
252
+});
packages/react/src/forwardRef.js
new
+27
@@ -0,0 +1,27 @@
1
+/**
2
+ * Copyright (c) 2014-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
+
8
+import {REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
9
+
10
+import warning from 'fbjs/lib/warning';
11
+
12
+export default function forwardRef<Props, ElementType: React$ElementType>(
13
+ render: (props: Props, ref: React$ElementRef<ElementType>) => React$Node,
14
+) {
15
+ if (__DEV__) {
16
+ warning(
17
+ typeof render === 'function',
18
+ 'forwardRef requires a render function but was given %s.',
19
+ render === null ? 'null' : typeof render,
20
+ );
21
+ }
22
+
23
+ return {
24
+ $$typeof: REACT_FORWARD_REF_TYPE,
25
+ render,
26
+ };
27
+}
packages/shared/ReactSymbols.js
+3
@@ -36,6 +36,9 @@ export const REACT_CONTEXT_TYPE = hasSymbol
36
export const REACT_ASYNC_MODE_TYPE = hasSymbol
37
? Symbol.for('react.async_mode')
38
: 0xeacf;
39
+export const REACT_FORWARD_REF_TYPE = hasSymbol
40
+ ? Symbol.for('react.forward_ref')
41
+ : 0xead0;
42
43
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
44
const FAUX_ITERATOR_SYMBOL = '@@iterator';
packages/shared/ReactTypeOfWork.js
+3
-1
@@ -21,7 +21,8 @@ export type TypeOfWork =
21
| 10
22
| 11
23
| 12
24
- | 13;
24
+ | 13
25
+ | 14;
26
27
export const IndeterminateComponent = 0; // Before we know whether it is functional or class
28
export const FunctionalComponent = 1;
@@ -37,3 +38,4 @@ export const Fragment = 10;
38
export const Mode = 11;
39
export const ContextConsumer = 12;
40
export const ContextProvider = 13;
41
+export const ForwardRef = 14;