Skip to content

[Fizz] Implement Classes #21200

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

Merged
merged 2 commits into from
Apr 8, 2021
Merged
Changes from all commits
Commits
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
673 changes: 673 additions & 0 deletions packages/react-server/src/ReactFizzClassComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,673 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import {emptyContextObject} from './ReactFizzContext';

import {disableLegacyContext} from 'shared/ReactFeatureFlags';
import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
import isArray from 'shared/isArray';

const didWarnAboutNoopUpdateForComponent = {};

let didWarnAboutUninitializedState;
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
let didWarnAboutLegacyLifecyclesAndDerivedState;
let didWarnAboutUndefinedDerivedState;
let warnOnUndefinedDerivedState;
let warnOnInvalidCallback;
let didWarnAboutDirectlyAssigningPropsToState;
let didWarnAboutContextTypeAndContextTypes;
let didWarnAboutInvalidateContextType;

if (__DEV__) {
didWarnAboutUninitializedState = new Set();
didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set();
didWarnAboutLegacyLifecyclesAndDerivedState = new Set();
didWarnAboutDirectlyAssigningPropsToState = new Set();
didWarnAboutUndefinedDerivedState = new Set();
didWarnAboutContextTypeAndContextTypes = new Set();
didWarnAboutInvalidateContextType = new Set();

const didWarnOnInvalidCallback = new Set();

warnOnInvalidCallback = function(callback: mixed, callerName: string) {
if (callback === null || typeof callback === 'function') {
return;
}
const key = callerName + '_' + (callback: any);
if (!didWarnOnInvalidCallback.has(key)) {
didWarnOnInvalidCallback.add(key);
console.error(
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
callback,
);
}
};

warnOnUndefinedDerivedState = function(type, partialState) {
if (partialState === undefined) {
const componentName = getComponentNameFromType(type) || 'Component';
if (!didWarnAboutUndefinedDerivedState.has(componentName)) {
didWarnAboutUndefinedDerivedState.add(componentName);
console.error(
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
'You have returned undefined.',
componentName,
);
}
}
};
}

function warnNoop(
publicInstance: React$Component<any, any>,
callerName: string,
) {
if (__DEV__) {
const constructor = publicInstance.constructor;
const componentName =
(constructor && getComponentNameFromType(constructor)) || 'ReactClass';
const warningKey = componentName + '.' + callerName;
if (didWarnAboutNoopUpdateForComponent[warningKey]) {
return;
}

console.error(
'%s(...): Can only update a mounting component. ' +
'This usually means you called %s() outside componentWillMount() on the server. ' +
'This is a no-op.\n\nPlease check the code for the %s component.',
callerName,
callerName,
componentName,
);
didWarnAboutNoopUpdateForComponent[warningKey] = true;
}
}

type InternalInstance = {
queue: null | Array<Object>,
replace: boolean,
};

const classComponentUpdater = {
isMounted(inst) {
return false;
},
enqueueSetState(inst, payload, callback) {
const internals: InternalInstance = getInstance(inst);
if (internals.queue === null) {
warnNoop(inst, 'setState');
} else {
internals.queue.push(payload);
if (__DEV__) {
if (callback !== undefined && callback !== null) {
warnOnInvalidCallback(callback, 'setState');
}
}
}
},
enqueueReplaceState(inst, payload, callback) {
const internals: InternalInstance = getInstance(inst);
internals.replace = true;
internals.queue = [payload];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no queue === null warnNoop branch here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t know. It’s not in the old one but it does seem like it should.

if (__DEV__) {
if (callback !== undefined && callback !== null) {
warnOnInvalidCallback(callback, 'setState');
}
}
},
enqueueForceUpdate(inst, callback) {
const internals: InternalInstance = getInstance(inst);
if (internals.queue === null) {
warnNoop(inst, 'forceUpdate');
} else {
if (__DEV__) {
if (callback !== undefined && callback !== null) {
warnOnInvalidCallback(callback, 'setState');
}
}
}
},
};

function applyDerivedStateFromProps(
instance: any,
ctor: any,
getDerivedStateFromProps: (props: any, state: any) => any,
prevState: any,
nextProps: any,
) {
const partialState = getDerivedStateFromProps(nextProps, prevState);

if (__DEV__) {
warnOnUndefinedDerivedState(ctor, partialState);
}
// Merge the partial state and the previous state.
const newState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
return newState;
}

export function constructClassInstance(
ctor: any,
props: any,
maskedLegacyContext: any,
): any {
let context = emptyContextObject;
const contextType = ctor.contextType;

if (__DEV__) {
if ('contextType' in ctor) {
const isValid =
// Allow null for conditional declaration
contextType === null ||
(contextType !== undefined &&
contextType.$$typeof === REACT_CONTEXT_TYPE &&
contextType._context === undefined); // Not a <Context.Consumer>

if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) {
didWarnAboutInvalidateContextType.add(ctor);

let addendum = '';
if (contextType === undefined) {
addendum =
' However, it is set to undefined. ' +
'This can be caused by a typo or by mixing up named and default imports. ' +
'This can also happen due to a circular dependency, so ' +
'try moving the createContext() call to a separate file.';
} else if (typeof contextType !== 'object') {
addendum = ' However, it is set to a ' + typeof contextType + '.';
} else if (contextType.$$typeof === REACT_PROVIDER_TYPE) {
addendum = ' Did you accidentally pass the Context.Provider instead?';
} else if (contextType._context !== undefined) {
// <Context.Consumer>
addendum = ' Did you accidentally pass the Context.Consumer instead?';
} else {
addendum =
' However, it is set to an object with keys {' +
Object.keys(contextType).join(', ') +
'}.';
}
console.error(
'%s defines an invalid contextType. ' +
'contextType should point to the Context object returned by React.createContext().%s',
getComponentNameFromType(ctor) || 'Component',
addendum,
);
}
}
}

if (typeof contextType === 'object' && contextType !== null) {
// TODO: Implement Context.
// context = readContext((contextType: any));
throw new Error('Context is not yet implemented.');
} else if (!disableLegacyContext) {
context = maskedLegacyContext;
}

const instance = new ctor(props, context);

if (__DEV__) {
if (
typeof ctor.getDerivedStateFromProps === 'function' &&
(instance.state === null || instance.state === undefined)
) {
const componentName = getComponentNameFromType(ctor) || 'Component';
if (!didWarnAboutUninitializedState.has(componentName)) {
didWarnAboutUninitializedState.add(componentName);
console.error(
'`%s` uses `getDerivedStateFromProps` but its initial state is ' +
'%s. This is not recommended. Instead, define the initial state by ' +
'assigning an object to `this.state` in the constructor of `%s`. ' +
'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.',
componentName,
instance.state === null ? 'null' : 'undefined',
componentName,
);
}
}

// If new component APIs are defined, "unsafe" lifecycles won't be called.
// Warn about these lifecycles if they are present.
// Don't warn about react-lifecycles-compat polyfilled methods though.
if (
typeof ctor.getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function'
) {
let foundWillMountName = null;
let foundWillReceivePropsName = null;
let foundWillUpdateName = null;
if (
typeof instance.componentWillMount === 'function' &&
instance.componentWillMount.__suppressDeprecationWarning !== true
) {
foundWillMountName = 'componentWillMount';
} else if (typeof instance.UNSAFE_componentWillMount === 'function') {
foundWillMountName = 'UNSAFE_componentWillMount';
}
if (
typeof instance.componentWillReceiveProps === 'function' &&
instance.componentWillReceiveProps.__suppressDeprecationWarning !== true
) {
foundWillReceivePropsName = 'componentWillReceiveProps';
} else if (
typeof instance.UNSAFE_componentWillReceiveProps === 'function'
) {
foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';
}
if (
typeof instance.componentWillUpdate === 'function' &&
instance.componentWillUpdate.__suppressDeprecationWarning !== true
) {
foundWillUpdateName = 'componentWillUpdate';
} else if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
foundWillUpdateName = 'UNSAFE_componentWillUpdate';
}
if (
foundWillMountName !== null ||
foundWillReceivePropsName !== null ||
foundWillUpdateName !== null
) {
const componentName = getComponentNameFromType(ctor) || 'Component';
const newApiName =
typeof ctor.getDerivedStateFromProps === 'function'
? 'getDerivedStateFromProps()'
: 'getSnapshotBeforeUpdate()';
if (!didWarnAboutLegacyLifecyclesAndDerivedState.has(componentName)) {
didWarnAboutLegacyLifecyclesAndDerivedState.add(componentName);
console.error(
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
'%s uses %s but also contains the following legacy lifecycles:%s%s%s\n\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://reactjs.org/link/unsafe-component-lifecycles',
componentName,
newApiName,
foundWillMountName !== null ? `\n ${foundWillMountName}` : '',
foundWillReceivePropsName !== null
? `\n ${foundWillReceivePropsName}`
: '',
foundWillUpdateName !== null ? `\n ${foundWillUpdateName}` : '',
);
}
}
}
}

return instance;
}

function checkClassInstance(instance: any, ctor: any, newProps: any) {
if (__DEV__) {
const name = getComponentNameFromType(ctor) || 'Component';
const renderPresent = instance.render;

if (!renderPresent) {
if (ctor.prototype && typeof ctor.prototype.render === 'function') {
console.error(
'%s(...): No `render` method found on the returned component ' +
'instance: did you accidentally return an object from the constructor?',
name,
);
} else {
console.error(
'%s(...): No `render` method found on the returned component ' +
'instance: you may have forgotten to define `render`.',
name,
);
}
}

if (
instance.getInitialState &&
!instance.getInitialState.isReactClassApproved &&
!instance.state
) {
console.error(
'getInitialState was defined on %s, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Did you mean to define a state property instead?',
name,
);
}
if (
instance.getDefaultProps &&
!instance.getDefaultProps.isReactClassApproved
) {
console.error(
'getDefaultProps was defined on %s, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Use a static property to define defaultProps instead.',
name,
);
}
if (instance.propTypes) {
console.error(
'propTypes was defined as an instance property on %s. Use a static ' +
'property to define propTypes instead.',
name,
);
}
if (instance.contextType) {
console.error(
'contextType was defined as an instance property on %s. Use a static ' +
'property to define contextType instead.',
name,
);
}

if (disableLegacyContext) {
if (ctor.childContextTypes) {
console.error(
'%s uses the legacy childContextTypes API which is no longer supported. ' +
'Use React.createContext() instead.',
name,
);
}
if (ctor.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which is no longer supported. ' +
'Use React.createContext() with static contextType instead.',
name,
);
}
} else {
if (instance.contextTypes) {
console.error(
'contextTypes was defined as an instance property on %s. Use a static ' +
'property to define contextTypes instead.',
name,
);
}

if (
ctor.contextType &&
ctor.contextTypes &&
!didWarnAboutContextTypeAndContextTypes.has(ctor)
) {
didWarnAboutContextTypeAndContextTypes.add(ctor);
console.error(
'%s declares both contextTypes and contextType static properties. ' +
'The legacy contextTypes property will be ignored.',
name,
);
}
}

if (typeof instance.componentShouldUpdate === 'function') {
console.error(
'%s has a method called ' +
'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
'The name is phrased as a question because the function is ' +
'expected to return a value.',
name,
);
}
if (
ctor.prototype &&
ctor.prototype.isPureReactComponent &&
typeof instance.shouldComponentUpdate !== 'undefined'
) {
console.error(
'%s has a method called shouldComponentUpdate(). ' +
'shouldComponentUpdate should not be used when extending React.PureComponent. ' +
'Please extend React.Component if shouldComponentUpdate is used.',
getComponentNameFromType(ctor) || 'A pure component',
);
}
if (typeof instance.componentDidUnmount === 'function') {
console.error(
'%s has a method called ' +
'componentDidUnmount(). But there is no such lifecycle method. ' +
'Did you mean componentWillUnmount()?',
name,
);
}
if (typeof instance.componentDidReceiveProps === 'function') {
console.error(
'%s has a method called ' +
'componentDidReceiveProps(). But there is no such lifecycle method. ' +
'If you meant to update the state in response to changing props, ' +
'use componentWillReceiveProps(). If you meant to fetch data or ' +
'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
name,
);
}
if (typeof instance.componentWillRecieveProps === 'function') {
console.error(
'%s has a method called ' +
'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
name,
);
}
if (typeof instance.UNSAFE_componentWillRecieveProps === 'function') {
console.error(
'%s has a method called ' +
'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?',
name,
);
}
const hasMutatedProps = instance.props !== newProps;
if (instance.props !== undefined && hasMutatedProps) {
console.error(
'%s(...): When calling super() in `%s`, make sure to pass ' +
"up the same props that your component's constructor was passed.",
name,
name,
);
}
if (instance.defaultProps) {
console.error(
'Setting defaultProps as an instance property on %s is not supported and will be ignored.' +
' Instead, define defaultProps as a static property on %s.',
name,
name,
);
}

if (
typeof instance.getSnapshotBeforeUpdate === 'function' &&
typeof instance.componentDidUpdate !== 'function' &&
!didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(ctor)
) {
didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(ctor);
console.error(
'%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' +
'This component defines getSnapshotBeforeUpdate() only.',
getComponentNameFromType(ctor),
);
}

if (typeof instance.getDerivedStateFromProps === 'function') {
console.error(
'%s: getDerivedStateFromProps() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
name,
);
}
if (typeof instance.getDerivedStateFromError === 'function') {
console.error(
'%s: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.',
name,
);
}
if (typeof ctor.getSnapshotBeforeUpdate === 'function') {
console.error(
'%s: getSnapshotBeforeUpdate() is defined as a static method ' +
'and will be ignored. Instead, declare it as an instance method.',
name,
);
}
const state = instance.state;
if (state && (typeof state !== 'object' || isArray(state))) {
console.error('%s.state: must be set to an object or null', name);
}
if (
typeof instance.getChildContext === 'function' &&
typeof ctor.childContextTypes !== 'object'
) {
console.error(
'%s.getChildContext(): childContextTypes must be defined in order to ' +
'use getChildContext().',
name,
);
}
}
}

function callComponentWillMount(type, instance) {
const oldState = instance.state;

if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
}
if (typeof instance.UNSAFE_componentWillMount === 'function') {
instance.UNSAFE_componentWillMount();
}

if (oldState !== instance.state) {
if (__DEV__) {
console.error(
'%s.componentWillMount(): Assigning directly to this.state is ' +
"deprecated (except inside a component's " +
'constructor). Use setState instead.',
getComponentNameFromType(type) || 'Component',
);
}
classComponentUpdater.enqueueReplaceState(instance, instance.state, null);
}
}

function processUpdateQueue(
internalInstance: InternalInstance,
inst: any,
props: any,
maskedLegacyContext: any,
): void {
if (internalInstance.queue !== null && internalInstance.queue.length > 0) {
const oldQueue = internalInstance.queue;
const oldReplace = internalInstance.replace;
internalInstance.queue = null;
internalInstance.replace = false;

if (oldReplace && oldQueue.length === 1) {
inst.state = oldQueue[0];
} else {
let nextState = oldReplace ? oldQueue[0] : inst.state;
let dontMutate = true;
for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
const partial = oldQueue[i];
const partialState =
typeof partial === 'function'
? partial.call(inst, nextState, props, maskedLegacyContext)
: partial;
if (partialState != null) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
inst.state = nextState;
}
} else {
internalInstance.queue = null;
}
}

// Invokes the mount life-cycles on a previously never rendered instance.
export function mountClassInstance(
instance: any,
ctor: any,
newProps: any,
maskedLegacyContext: any,
): void {
if (__DEV__) {
checkClassInstance(instance, ctor, newProps);
}

const initialState = instance.state !== undefined ? instance.state : null;

instance.updater = classComponentUpdater;
instance.props = newProps;
instance.state = initialState;
// We don't bother initializing the refs object on the server, since we're not going to resolve them anyway.

// The internal instance will be used to manage updates that happen during this mount.
const internalInstance: InternalInstance = {
queue: null,
replace: false,
};
setInstance(instance, internalInstance);

const contextType = ctor.contextType;
if (typeof contextType === 'object' && contextType !== null) {
// TODO: Implement Context.
// instance.context = readContext(contextType);
throw new Error('Context is not yet implemented.');
} else if (disableLegacyContext) {
instance.context = emptyContextObject;
} else {
instance.context = maskedLegacyContext;
}

if (__DEV__) {
if (instance.state === newProps) {
const componentName = getComponentNameFromType(ctor) || 'Component';
if (!didWarnAboutDirectlyAssigningPropsToState.has(componentName)) {
didWarnAboutDirectlyAssigningPropsToState.add(componentName);
console.error(
'%s: It is not recommended to assign props directly to state ' +
"because updates to props won't be reflected in state. " +
'In most cases, it is better to use props directly.',
componentName,
);
}
}
}

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
instance.state = applyDerivedStateFromProps(
instance,
ctor,
getDerivedStateFromProps,
initialState,
newProps,
);
}

// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof ctor.getDerivedStateFromProps !== 'function' &&
typeof instance.getSnapshotBeforeUpdate !== 'function' &&
(typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function')
) {
callComponentWillMount(ctor, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
processUpdateQueue(
internalInstance,
instance,
newProps,
maskedLegacyContext,
);
}
}
94 changes: 94 additions & 0 deletions packages/react-server/src/ReactFizzContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import {disableLegacyContext} from 'shared/ReactFeatureFlags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import invariant from 'shared/invariant';
import checkPropTypes from 'shared/checkPropTypes';

let warnedAboutMissingGetChildContext;

if (__DEV__) {
warnedAboutMissingGetChildContext = {};
}

export const emptyContextObject = {};
if (__DEV__) {
Object.freeze(emptyContextObject);
}

export function getMaskedContext(type: any, unmaskedContext: Object): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
const contextTypes = type.contextTypes;
if (!contextTypes) {
return emptyContextObject;
}

const context = {};
for (const key in contextTypes) {
context[key] = unmaskedContext[key];
}

if (__DEV__) {
const name = getComponentNameFromType(type) || 'Unknown';
checkPropTypes(contextTypes, context, 'context', name);
}

return context;
}
}

export function processChildContext(
type: any,
instance: any,
parentContext: Object,
childContextTypes: Object,
): Object {
if (disableLegacyContext) {
return parentContext;
} else {
// TODO (bvaughn) Replace this behavior with an invariant() in the future.
// It has only been added in Fiber to match the (unintentional) behavior in Stack.
if (typeof instance.getChildContext !== 'function') {
if (__DEV__) {
const componentName = getComponentNameFromType(type) || 'Unknown';

if (!warnedAboutMissingGetChildContext[componentName]) {
warnedAboutMissingGetChildContext[componentName] = true;
console.error(
'%s.childContextTypes is specified but there is no getChildContext() method ' +
'on the instance. You can either define getChildContext() on %s or remove ' +
'childContextTypes from it.',
componentName,
componentName,
);
}
}
return parentContext;
}

const childContext = instance.getChildContext();
for (const contextKey in childContext) {
invariant(
contextKey in childContextTypes,
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
getComponentNameFromType(type) || 'Unknown',
contextKey,
);
}
if (__DEV__) {
const name = getComponentNameFromType(type) || 'Unknown';
checkPropTypes(childContextTypes, childContext, 'child context', name);
}

return {...parentContext, ...childContext};
}
}
288 changes: 275 additions & 13 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
@@ -47,14 +47,33 @@ import {
createSuspenseBoundaryID,
getChildFormatContext,
} from './ReactServerFormatConfig';
import {
constructClassInstance,
mountClassInstance,
} from './ReactFizzClassComponent';
import {
getMaskedContext,
processChildContext,
emptyContextObject,
} from './ReactFizzContext';
import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
disableLegacyContext,
disableModulePatternComponents,
warnAboutDefaultPropsOnFunctionComponents,
} from 'shared/ReactFeatureFlags';

import getComponentNameFromType from 'shared/getComponentNameFromType';
import invariant from 'shared/invariant';
import isArray from 'shared/isArray';

const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;

type LegacyContext = {
[key: string]: any,
};

type SuspenseBoundary = {
+id: SuspenseBoundaryID,
rootSegmentID: number,
@@ -72,6 +91,7 @@ type Task = {
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment, // the segment we'll write to
abortSet: Set<Task>, // the abortable set that this task belongs to
legacyContext: LegacyContext, // the current legacy context that this task is executing in
assignID: null | SuspenseBoundaryID, // id to assign to the content
};

@@ -151,7 +171,7 @@ export function createRequest(
children: ReactNodeList,
destination: Destination,
responseState: ResponseState,
rootContext: FormatContext,
rootFormatContext: FormatContext,
progressiveChunkSize: number = DEFAULT_PROGRESSIVE_CHUNK_SIZE,
onError: (error: mixed) => void = defaultErrorHandler,
onCompleteAll: () => void = noop,
@@ -178,7 +198,7 @@ export function createRequest(
onReadyToStream,
};
// This segment represents the root fallback.
const rootSegment = createPendingSegment(request, 0, null, rootContext);
const rootSegment = createPendingSegment(request, 0, null, rootFormatContext);
// There is no parent so conceptually, we're unblocked to flush this segment.
rootSegment.parentFlushed = true;
const rootTask = createTask(
@@ -187,6 +207,7 @@ export function createRequest(
null,
rootSegment,
abortSet,
emptyContextObject,
null,
);
pingedTasks.push(rootTask);
@@ -223,6 +244,7 @@ function createTask(
blockedBoundary: Root | SuspenseBoundary,
blockedSegment: Segment,
abortSet: Set<Task>,
legacyContext: LegacyContext,
assignID: null | SuspenseBoundaryID,
): Task {
request.allPendingTasks++;
@@ -237,6 +259,7 @@ function createTask(
blockedBoundary,
blockedSegment,
abortSet,
legacyContext,
assignID,
};
abortSet.add(task);
@@ -350,17 +373,18 @@ function renderSuspenseBoundary(
task.blockedSegment = parentSegment;
}

// We create suspended task for the fallback because we don't want to actually task
// We create suspended task for the fallback because we don't want to actually work
// on it yet in case we finish the main content, so we queue for later.
const suspendedFallbackTask = createTask(
request,
fallback,
parentBoundary,
boundarySegment,
fallbackAbortSet,
task.legacyContext,
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
);
// TODO: This should be queued at a separate lower priority queue so that we only task
// TODO: This should be queued at a separate lower priority queue so that we only work
// on preparing fallbacks if we don't have any more main content to task on.
request.pingedTasks.push(suspendedFallbackTask);
}
@@ -393,16 +417,247 @@ function renderHostElement(
pushEndInstance(segment.chunks, type, props);
}

function renderFunctionComponent(
function shouldConstruct(Component) {
return Component.prototype && Component.prototype.isReactComponent;
}

function renderWithHooks<Props, SecondArg>(
request: Request,
task: Task,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
): any {
// TODO: Set up Hooks etc.
const children = Component(props, secondArg);
return children;
}

function finishClassComponent(
request: Request,
task: Task,
type: (props: any) => ReactNodeList,
instance: Object,
Component: any,
props: any,
): ReactNodeList {
const nextChildren = instance.render();

if (__DEV__) {
if (instance.props !== props) {
if (!didWarnAboutReassigningProps) {
console.error(
'It looks like %s is reassigning its own `this.props` while rendering. ' +
'This is not supported and can lead to confusing bugs.',
getComponentNameFromType(Component) || 'a component',
);
}
didWarnAboutReassigningProps = true;
}
}

if (!disableLegacyContext) {
const childContextTypes = Component.childContextTypes;
if (childContextTypes !== null && childContextTypes !== undefined) {
const previousContext = task.legacyContext;
const mergedContext = processChildContext(
instance,
Component,
previousContext,
childContextTypes,
);
task.legacyContext = mergedContext;
renderNodeDestructive(request, task, nextChildren);
task.legacyContext = previousContext;
return;
}
}

renderNodeDestructive(request, task, nextChildren);
}

function renderClassComponent(
request: Request,
task: Task,
Component: any,
props: any,
): void {
const result = type(props);
// We're now successfully past this task, and we don't have to pop back to
// the previous task every again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, result);
const unmaskedContext = !disableLegacyContext
? task.legacyContext
: undefined;
const instance = constructClassInstance(Component, props, unmaskedContext);
mountClassInstance(instance, Component, props, unmaskedContext);
finishClassComponent(request, task, Component);
}

const didWarnAboutBadClass = {};
const didWarnAboutModulePatternComponent = {};
const didWarnAboutContextTypeOnFunctionComponent = {};
const didWarnAboutGetDerivedStateOnFunctionComponent = {};
let didWarnAboutReassigningProps = false;
const didWarnAboutDefaultPropsOnFunctionComponent = {};

// This would typically be a function component but we still support module pattern
// components for some reason.
function renderIndeterminateComponent(
request: Request,
task: Task,
Component: any,
props: any,
): void {
let legacyContext;
if (!disableLegacyContext) {
legacyContext = getMaskedContext(Component, task.legacyContext);
}

if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';

if (!didWarnAboutBadClass[componentName]) {
console.error(
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
}

const value = renderWithHooks(request, task, Component, props, legacyContext);

if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
// Whether or not it would crash later, we want to show a good message in DEV first.
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
}

if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
if (__DEV__) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}

mountClassInstance(value, Component, props, legacyContext);
finishClassComponent(request, task, value, Component);
} else {
// Proceed under the assumption that this is a function component
if (__DEV__) {
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which is no longer supported. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
if (__DEV__) {
validateFunctionComponentInDev(Component);
}
// We're now successfully past this task, and we don't have to pop back to
// the previous task every again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, value);
}
}

function validateFunctionComponentInDev(Component: any): void {
if (__DEV__) {
if (Component) {
if (Component.childContextTypes) {
console.error(
'%s(...): childContextTypes cannot be defined on a function component.',
Component.displayName || Component.name || 'Component',
);
}
}

if (
warnAboutDefaultPropsOnFunctionComponents &&
Component.defaultProps !== undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';

if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
console.error(
'%s: Support for defaultProps will be removed from function components ' +
'in a future major release. Use JavaScript default parameters instead.',
componentName,
);
didWarnAboutDefaultPropsOnFunctionComponent[componentName] = true;
}
}

if (typeof Component.getDerivedStateFromProps === 'function') {
const componentName = getComponentNameFromType(Component) || 'Unknown';

if (!didWarnAboutGetDerivedStateOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionComponent[componentName] = true;
}
}

if (
typeof Component.contextType === 'object' &&
Component.contextType !== null
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';

if (!didWarnAboutContextTypeOnFunctionComponent[componentName]) {
console.error(
'%s: Function components do not support contextType.',
componentName,
);
didWarnAboutContextTypeOnFunctionComponent[componentName] = true;
}
}
}
}

function renderElement(
@@ -413,7 +668,11 @@ function renderElement(
node: ReactNodeList,
): void {
if (typeof type === 'function') {
renderFunctionComponent(request, task, type, props);
if (shouldConstruct(type)) {
renderClassComponent(request, task, type, props);
} else {
renderIndeterminateComponent(request, task, type, props);
}
} else if (typeof type === 'string') {
renderHostElement(request, task, type, props);
} else if (type === REACT_SUSPENSE_TYPE) {
@@ -505,6 +764,7 @@ function spawnNewSuspendedTask(
task.blockedBoundary,
newSegment,
task.abortSet,
task.legacyContext,
task.assignID,
);
// We've delegated the assignment.
@@ -521,15 +781,17 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {

// Snapshot the current context in case something throws to interrupt the
// process.
const previousContext = task.blockedSegment.formatContext;
const previousFormatContext = task.blockedSegment.formatContext;
const previousLegacyContext = task.legacyContext;
try {
return renderNodeDestructive(request, task, node);
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
spawnNewSuspendedTask(request, task, x);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.blockedSegment.formatContext = previousContext;
task.blockedSegment.formatContext = previousFormatContext;
task.legacyContext = previousLegacyContext;
} else {
// We assume that we don't need the correct context.
// Let's terminate the rest of the tree and don't render any siblings.