mirror of
https://github.com/facebook/react.git
synced 2026-02-27 03:07:57 +00:00
Added React.forwardRef support to react-reconciler based renders and the SSR partial renderer.
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
packages/react-is/src/ReactIs.js
vendored
6
packages/react-is/src/ReactIs.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
5
packages/react-reconciler/src/ReactFiber.js
vendored
5
packages/react-reconciler/src/ReactFiber.js
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
252
packages/react/src/__tests__/forwardRef-test.internal.js
Normal file
252
packages/react/src/__tests__/forwardRef-test.internal.js
Normal 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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
27
packages/react/src/forwardRef.js
Normal file
27
packages/react/src/forwardRef.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user