RFC #30: React.forwardRef implementation (#12346)

Added React.forwardRef support to react-reconciler based renders and the SSR partial renderer.
This commit is contained in:
Brian Vaughn
2018-03-14 13:07:58 -07:00
committed by GitHub
parent 77196100b8
commit bc70441c8b
13 changed files with 368 additions and 2 deletions

View File

@@ -96,4 +96,25 @@ describe('ReactDOMServerIntegration', () => {
expect(component.refs.myDiv).toBe(root.firstChild);
});
});
it('should forward refs', async () => {
const divRef = React.createRef();
class InnerComponent extends React.Component {
render() {
return <div ref={this.props.forwardedRef}>{this.props.value}</div>;
}
}
const OuterComponent = React.forwardRef((props, ref) => (
<InnerComponent {...props} forwardedRef={ref} />
));
await clientRenderOnServerString(
<OuterComponent ref={divRef} value="hello" />,
);
expect(divRef.current).not.toBe(null);
expect(divRef.current.textContent).toBe('hello');
});
});

View File

@@ -27,6 +27,7 @@ import describeComponentFrame from 'shared/describeComponentFrame';
import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
import {
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_ASYNC_MODE_TYPE,
@@ -841,6 +842,25 @@ class ReactDOMServerRenderer {
}
if (typeof elementType === 'object' && elementType !== null) {
switch (elementType.$$typeof) {
case REACT_FORWARD_REF_TYPE: {
const element: ReactElement = ((nextChild: any): ReactElement);
const nextChildren = toArray(
elementType.render(element.props, element.ref),
);
const frame: Frame = {
type: null,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
footer: '',
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
return '';
}
case REACT_PROVIDER_TYPE: {
const provider: ReactProvider<any> = (nextChild: any);
const nextProps = provider.props;

View File

@@ -13,6 +13,7 @@ import {
REACT_ASYNC_MODE_TYPE,
REACT_CONTEXT_TYPE,
REACT_ELEMENT_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_PROVIDER_TYPE,
@@ -37,6 +38,7 @@ export function typeOf(object: any) {
switch ($$typeofType) {
case REACT_CONTEXT_TYPE:
case REACT_FORWARD_REF_TYPE:
case REACT_PROVIDER_TYPE:
return $$typeofType;
default:
@@ -55,6 +57,7 @@ export const AsyncMode = REACT_ASYNC_MODE_TYPE;
export const ContextConsumer = REACT_CONTEXT_TYPE;
export const ContextProvider = REACT_PROVIDER_TYPE;
export const Element = REACT_ELEMENT_TYPE;
export const ForwardRef = REACT_FORWARD_REF_TYPE;
export const Fragment = REACT_FRAGMENT_TYPE;
export const Portal = REACT_PORTAL_TYPE;
export const StrictMode = REACT_STRICT_MODE_TYPE;
@@ -75,6 +78,9 @@ export function isElement(object: any) {
object.$$typeof === REACT_ELEMENT_TYPE
);
}
export function isForwardRef(object: any) {
return typeOf(object) === REACT_FORWARD_REF_TYPE;
}
export function isFragment(object: any) {
return typeOf(object) === REACT_FRAGMENT_TYPE;
}

View File

@@ -76,6 +76,15 @@ describe('ReactIs', () => {
expect(ReactIs.isElement(<React.StrictMode />)).toBe(true);
});
it('should identify ref forwarding component', () => {
const RefForwardingComponent = React.forwardRef((props, ref) => null);
expect(ReactIs.typeOf(<RefForwardingComponent />)).toBe(ReactIs.ForwardRef);
expect(ReactIs.isForwardRef(<RefForwardingComponent />)).toBe(true);
expect(ReactIs.isForwardRef({type: ReactIs.StrictMode})).toBe(false);
expect(ReactIs.isForwardRef(<React.unstable_AsyncMode />)).toBe(false);
expect(ReactIs.isForwardRef(<div />)).toBe(false);
});
it('should identify fragments', () => {
expect(ReactIs.typeOf(<React.Fragment />)).toBe(ReactIs.Fragment);
expect(ReactIs.isFragment(<React.Fragment />)).toBe(true);

View File

@@ -25,6 +25,7 @@ import {
HostPortal,
CallComponent,
ReturnComponent,
ForwardRef,
Fragment,
Mode,
ContextProvider,
@@ -35,6 +36,7 @@ import getComponentName from 'shared/getComponentName';
import {NoWork} from './ReactFiberExpirationTime';
import {NoContext, AsyncMode, StrictMode} from './ReactTypeOfMode';
import {
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
REACT_RETURN_TYPE,
REACT_CALL_TYPE,
@@ -357,6 +359,9 @@ export function createFiberFromElement(
// This is a consumer
fiberTag = ContextConsumer;
break;
case REACT_FORWARD_REF_TYPE:
fiberTag = ForwardRef;
break;
default:
if (typeof type.tag === 'number') {
// Currently assumed to be a continuation and therefore is a

View File

@@ -26,6 +26,7 @@ import {
CallComponent,
CallHandlerPhase,
ReturnComponent,
ForwardRef,
Fragment,
Mode,
ContextProvider,
@@ -153,6 +154,17 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
}
}
function updateForwardRef(current, workInProgress) {
const render = workInProgress.type.render;
const nextChildren = render(
workInProgress.pendingProps,
workInProgress.ref,
);
reconcileChildren(current, workInProgress, nextChildren);
memoizeProps(workInProgress, nextChildren);
return workInProgress.child;
}
function updateFragment(current, workInProgress) {
const nextChildren = workInProgress.pendingProps;
if (hasLegacyContextChanged()) {
@@ -1130,6 +1142,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
workInProgress,
renderExpirationTime,
);
case ForwardRef:
return updateForwardRef(current, workInProgress);
case Fragment:
return updateFragment(current, workInProgress);
case Mode:

View File

@@ -32,6 +32,7 @@ import {
ReturnComponent,
ContextProvider,
ContextConsumer,
ForwardRef,
Fragment,
Mode,
} from 'shared/ReactTypeOfWork';
@@ -603,6 +604,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
case ReturnComponent:
// Does nothing.
return null;
case ForwardRef:
return null;
case Fragment:
return null;
case Mode:

View File

@@ -24,6 +24,7 @@ import {
isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
import forwardRef from './forwardRef';
import {
createElementWithValidation,
createFactoryWithValidation,
@@ -45,6 +46,7 @@ const React = {
PureComponent,
createContext,
forwardRef,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,

View File

@@ -22,6 +22,7 @@ import {
REACT_ASYNC_MODE_TYPE,
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_FORWARD_REF_TYPE,
} from 'shared/ReactSymbols';
import checkPropTypes from 'prop-types/checkPropTypes';
import warning from 'fbjs/lib/warning';
@@ -297,7 +298,8 @@ export function createElementWithValidation(type, props, children) {
(typeof type === 'object' &&
type !== null &&
(type.$$typeof === REACT_PROVIDER_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE));
type.$$typeof === REACT_CONTEXT_TYPE ||
type.$$typeof === REACT_FORWARD_REF_TYPE));
// We warn in this case but don't throw. We expect the element creation to
// succeed and there will likely be errors in render.

View File

@@ -0,0 +1,252 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
describe('forwardRef', () => {
let React;
let ReactFeatureFlags;
let ReactNoop;
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
React = require('react');
ReactNoop = require('react-noop-renderer');
});
it('should work without a ref to be forwarded', () => {
class Child extends React.Component {
render() {
ReactNoop.yield(this.props.value);
return null;
}
}
function Wrapper(props) {
return <Child {...props} ref={props.forwardedRef} />;
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<Wrapper {...props} forwardedRef={ref} />
));
ReactNoop.render(<RefForwardingComponent value={123} />);
expect(ReactNoop.flush()).toEqual([123]);
});
it('should forward a ref for a single child', () => {
class Child extends React.Component {
render() {
ReactNoop.yield(this.props.value);
return null;
}
}
function Wrapper(props) {
return <Child {...props} ref={props.forwardedRef} />;
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<Wrapper {...props} forwardedRef={ref} />
));
const ref = React.createRef();
ReactNoop.render(<RefForwardingComponent ref={ref} value={123} />);
expect(ReactNoop.flush()).toEqual([123]);
expect(ref.current instanceof Child).toBe(true);
});
it('should forward a ref for multiple children', () => {
class Child extends React.Component {
render() {
ReactNoop.yield(this.props.value);
return null;
}
}
function Wrapper(props) {
return <Child {...props} ref={props.forwardedRef} />;
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<Wrapper {...props} forwardedRef={ref} />
));
const ref = React.createRef();
ReactNoop.render(
<div>
<div />
<RefForwardingComponent ref={ref} value={123} />
<div />
</div>,
);
expect(ReactNoop.flush()).toEqual([123]);
expect(ref.current instanceof Child).toBe(true);
});
it('should update refs when switching between children', () => {
function FunctionalComponent({forwardedRef, setRefOnDiv}) {
return (
<section>
<div ref={setRefOnDiv ? forwardedRef : null}>First</div>
<span ref={setRefOnDiv ? null : forwardedRef}>Second</span>
</section>
);
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<FunctionalComponent {...props} forwardedRef={ref} />
));
const ref = React.createRef();
ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={true} />);
ReactNoop.flush();
expect(ref.current.type).toBe('div');
ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={false} />);
ReactNoop.flush();
expect(ref.current.type).toBe('span');
});
it('should maintain child instance and ref through updates', () => {
class Child extends React.Component {
constructor(props) {
super(props);
}
render() {
ReactNoop.yield(this.props.value);
return null;
}
}
function Wrapper(props) {
return <Child {...props} ref={props.forwardedRef} />;
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<Wrapper {...props} forwardedRef={ref} />
));
let setRefCount = 0;
let ref;
const setRef = r => {
setRefCount++;
ref = r;
};
ReactNoop.render(<RefForwardingComponent ref={setRef} value={123} />);
expect(ReactNoop.flush()).toEqual([123]);
expect(ref instanceof Child).toBe(true);
expect(setRefCount).toBe(1);
ReactNoop.render(<RefForwardingComponent ref={setRef} value={456} />);
expect(ReactNoop.flush()).toEqual([456]);
expect(ref instanceof Child).toBe(true);
expect(setRefCount).toBe(1);
});
it('should not break lifecycle error handling', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
ReactNoop.yield('ErrorBoundary.componentDidCatch');
this.setState({error});
}
render() {
if (this.state.error) {
ReactNoop.yield('ErrorBoundary.render: catch');
return null;
}
ReactNoop.yield('ErrorBoundary.render: try');
return this.props.children;
}
}
class BadRender extends React.Component {
render() {
ReactNoop.yield('BadRender throw');
throw new Error('oops!');
}
}
function Wrapper(props) {
ReactNoop.yield('Wrapper');
return <BadRender {...props} ref={props.forwardedRef} />;
}
const RefForwardingComponent = React.forwardRef((props, ref) => (
<Wrapper {...props} forwardedRef={ref} />
));
const ref = React.createRef();
ReactNoop.render(
<ErrorBoundary>
<RefForwardingComponent ref={ref} />
</ErrorBoundary>,
);
expect(ReactNoop.flush()).toEqual([
'ErrorBoundary.render: try',
'Wrapper',
'BadRender throw',
'ErrorBoundary.componentDidCatch',
'ErrorBoundary.render: catch',
]);
expect(ref.current).toBe(null);
});
it('should support rendering null', () => {
const RefForwardingComponent = React.forwardRef((props, ref) => null);
const ref = React.createRef();
ReactNoop.render(<RefForwardingComponent ref={ref} />);
ReactNoop.flush();
expect(ref.current).toBe(null);
});
it('should support rendering null for multiple children', () => {
const RefForwardingComponent = React.forwardRef((props, ref) => null);
const ref = React.createRef();
ReactNoop.render(
<div>
<div />
<RefForwardingComponent ref={ref} />
<div />
</div>,
);
ReactNoop.flush();
expect(ref.current).toBe(null);
});
it('should warn if not provided a callback during creation', () => {
expect(() => React.forwardRef(undefined)).toWarnDev(
'forwardRef requires a render function but was given undefined.',
);
expect(() => React.forwardRef(null)).toWarnDev(
'forwardRef requires a render function but was given null.',
);
expect(() => React.forwardRef('foo')).toWarnDev(
'forwardRef requires a render function but was given string.',
);
});
it('should warn if no render function is provided', () => {
expect(React.forwardRef).toWarnDev(
'forwardRef requires a render function but was given undefined.',
);
});
});

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
import warning from 'fbjs/lib/warning';
export default function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$ElementRef<ElementType>) => React$Node,
) {
if (__DEV__) {
warning(
typeof render === 'function',
'forwardRef requires a render function but was given %s.',
render === null ? 'null' : typeof render,
);
}
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
}

View File

@@ -36,6 +36,9 @@ export const REACT_CONTEXT_TYPE = hasSymbol
export const REACT_ASYNC_MODE_TYPE = hasSymbol
? Symbol.for('react.async_mode')
: 0xeacf;
export const REACT_FORWARD_REF_TYPE = hasSymbol
? Symbol.for('react.forward_ref')
: 0xead0;
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';

View File

@@ -21,7 +21,8 @@ export type TypeOfWork =
| 10
| 11
| 12
| 13;
| 13
| 14;
export const IndeterminateComponent = 0; // Before we know whether it is functional or class
export const FunctionalComponent = 1;
@@ -37,3 +38,4 @@ export const Fragment = 10;
export const Mode = 11;
export const ContextConsumer = 12;
export const ContextProvider = 13;
export const ForwardRef = 14;