@samitouri / QOS-React / commits / bc70441c8b

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;