From cf45a623a1787cf57094eb7d2f66903d358647fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 8 Apr 2021 13:42:37 -0400 Subject: [PATCH] [Fizz] Implement Classes (#21200) * Legacy context * Port Classes from Fiber to Fizz --- .../src/ReactFizzClassComponent.js | 673 ++++++++++++++++++ packages/react-server/src/ReactFizzContext.js | 94 +++ packages/react-server/src/ReactFizzServer.js | 288 +++++++- 3 files changed, 1042 insertions(+), 13 deletions(-) create mode 100644 packages/react-server/src/ReactFizzClassComponent.js create mode 100644 packages/react-server/src/ReactFizzContext.js diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js new file mode 100644 index 0000000000000..d56bc633b8498 --- /dev/null +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -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, + 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, + 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]; + 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 + + 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) { + // + 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, + ); + } +} diff --git a/packages/react-server/src/ReactFizzContext.js b/packages/react-server/src/ReactFizzContext.js new file mode 100644 index 0000000000000..85751e779d11f --- /dev/null +++ b/packages/react-server/src/ReactFizzContext.js @@ -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}; + } +} diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index c1293c8ba4b4d..4d94aa5945fbb 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -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, // 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, + legacyContext: LegacyContext, assignID: null | SuspenseBoundaryID, ): Task { request.allPendingTasks++; @@ -237,6 +259,7 @@ function createTask( blockedBoundary, blockedSegment, abortSet, + legacyContext, assignID, }; abortSet.add(task); @@ -350,7 +373,7 @@ 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, @@ -358,9 +381,10 @@ function renderSuspenseBoundary( 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( + 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,7 +781,8 @@ 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) { @@ -529,7 +790,8 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { 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.