Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC #30: React.forwardRef implementation #12346

Merged
merged 22 commits into from
Mar 14, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.value).not.toBe(null);
expect(divRef.value.textContent).toBe('hello');
});
});
20 changes: 20 additions & 0 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions packages/react-is/src/ReactIs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/react-is/src/__tests__/ReactIs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
HostPortal,
CallComponent,
ReturnComponent,
ForwardRef,
Fragment,
Mode,
ContextProvider,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
CallComponent,
CallHandlerPhase,
ReturnComponent,
ForwardRef,
Fragment,
Mode,
ContextProvider,
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ReturnComponent,
ContextProvider,
ContextConsumer,
ForwardRef,
Fragment,
Mode,
} from 'shared/ReactTypeOfWork';
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
import forwardRef from './forwardRef';
import {
createElementWithValidation,
createFactoryWithValidation,
Expand All @@ -45,6 +46,7 @@ const React = {
PureComponent,

createContext,
forwardRef,

Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/ReactElementValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
Loading