From b48b2594fae7af3228ff6a0727cf8828b33b419e Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Thu, 25 May 2017 15:12:17 -0700 Subject: [PATCH] Cherrypick warning and removal of react create class (#9771) * react-create-class -> create-react-class * Fix issues/bugs introduced by merge conflict resolution **what is the change?:** A couple of bugs and holes were introduced when cherry-picking https://github.com/facebook/react/pull/9232 onto the 15.6 branch. This fixes them. We also needed to add some logic from https://github.com/facebook/react/pull/9399 **why make this change?:** To keep tests passing and get this change working. **test plan:** `yarn test` **issue:** https://github.com/facebook/react/issues/9398 * Move component base classes into a single file (#8918) * More fixes for issues introduced by rebasing **what is the change?:** - Remove some outdated 'require' statements that got orphaned in 'React.js' - Change 'warning' to 'lowPriorityWarning' for 'React.createClass' - Fix syntax issues in 'React-test' - Use 'creatReactClass' instead of ES6 class in ReactART - Update 'prop-type' dependency to use no higher than 15.7 because 15.8 limits the number of warnings, and this causes a test to fail. - Fix some mixed-up and misnamed variables in `React.js` - Rebase onto commit that updates deprecation messages - Update a test based on new deprecation messages **why make this change?:** These were bugs introduced by rebasing and tests caught the regressions. **test plan:** `yarn test` **issue:** https://github.com/facebook/react/issues/9398 * Reset `yarn.lock` **what is the change?:** I didn't mean to commit changes to `yarn.lock` except for the `prop-types` and `create-react-class` updates. **why make this change?:** To minimize the changes we make to dependency versions. **test plan:** `rm -rf node_modules` `yarn install` `yarn run build` `yarn test` * Run `yarn prettier` --- package.json | 6 +- src/isomorphic/React.js | 27 +- src/isomorphic/__tests__/React-test.js | 30 + src/isomorphic/classic/class/ReactClass.js | 867 ------------------ ...=> create-react-class-integration-test.js} | 164 ++-- src/isomorphic/classic/class/createClass.js | 19 + ...{ReactComponent.js => ReactBaseClasses.js} | 28 +- .../modern/class/ReactPureComponent.js | 40 - src/renderers/art/ReactART.js | 3 +- .../wrappers/__tests__/ReactDOMInput-test.js | 20 +- yarn.lock | 10 +- 11 files changed, 225 insertions(+), 989 deletions(-) delete mode 100644 src/isomorphic/classic/class/ReactClass.js rename src/isomorphic/classic/class/__tests__/{ReactClass-test.js => create-react-class-integration-test.js} (74%) create mode 100644 src/isomorphic/classic/class/createClass.js rename src/isomorphic/modern/class/{ReactComponent.js => ReactBaseClasses.js} (83%) delete mode 100644 src/isomorphic/modern/class/ReactPureComponent.js diff --git a/package.json b/package.json index 96f4beb4f348..8d5abfe84af8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "coffee-script": "^1.8.0", "core-js": "^2.2.1", "coveralls": "^2.11.6", - "create-react-class": "15.5.0", "del": "^2.0.2", "derequire": "^2.0.3", "eslint": "^3.10.2", @@ -72,7 +71,6 @@ "object-assign": "^4.1.1", "platform": "^1.1.0", "prettier": "^1.2.2", - "prop-types": "15.5.7", "run-sequence": "^1.1.4", "through2": "^2.0.0", "tmp": "~0.0.28", @@ -85,6 +83,10 @@ "node": "4.x || 5.x || 6.x || 7.x", "npm": "2.x || 3.x || 4.x" }, + "dependencies": { + "create-react-class": "^15.5.2", + "prop-types": "15.5.7" + }, "commonerConfig": { "version": 7 }, diff --git a/src/isomorphic/React.js b/src/isomorphic/React.js index 68662037b96d..3df4df456bb6 100644 --- a/src/isomorphic/React.js +++ b/src/isomorphic/React.js @@ -11,15 +11,14 @@ 'use strict'; +var ReactBaseClasses = require('ReactBaseClasses'); var ReactChildren = require('ReactChildren'); -var ReactComponent = require('ReactComponent'); -var ReactPureComponent = require('ReactPureComponent'); -var ReactClass = require('ReactClass'); var ReactDOMFactories = require('ReactDOMFactories'); var ReactElement = require('ReactElement'); var ReactPropTypes = require('ReactPropTypes'); var ReactVersion = require('ReactVersion'); +var createReactClass = require('createClass'); var onlyChild = require('onlyChild'); var createElement = ReactElement.createElement; @@ -80,8 +79,8 @@ var React = { only: onlyChild, }, - Component: ReactComponent, - PureComponent: ReactPureComponent, + Component: ReactBaseClasses.Component, + PureComponent: ReactBaseClasses.PureComponent, createElement: createElement, cloneElement: cloneElement, @@ -90,7 +89,7 @@ var React = { // Classic PropTypes: ReactPropTypes, - createClass: ReactClass.createClass, + createClass: createReactClass, createFactory: createFactory, createMixin: createMixin, @@ -104,8 +103,8 @@ var React = { __spread: __spread, }; -// TODO: Fix tests so that this deprecation warning doesn't cause failures. if (__DEV__) { + let warnedForCreateClass = false; if (canDefineProperty) { Object.defineProperty(React, 'PropTypes', { get() { @@ -122,6 +121,20 @@ if (__DEV__) { return ReactPropTypes; }, }); + + Object.defineProperty(React, 'createClass', { + get: function() { + lowPriorityWarning( + warnedForCreateClass, + 'React.createClass is no longer supported. Use a plain JavaScript ' + + "class instead. If you're not yet ready to migrate, " + + 'create-react-class is available on npm as a temporary, ' + + 'drop-in replacement.', + ); + warnedForCreateClass = true; + return createReactClass; + }, + }); } // React.DOM factories are deprecated. Wrap these methods so that diff --git a/src/isomorphic/__tests__/React-test.js b/src/isomorphic/__tests__/React-test.js index 7639bb107529..9cd9ebb18af7 100644 --- a/src/isomorphic/__tests__/React-test.js +++ b/src/isomorphic/__tests__/React-test.js @@ -37,4 +37,34 @@ describe('React', () => { 'React.createMixin is deprecated and should not be used', ); }); + + it('should warn once when attempting to access React.createClass', () => { + spyOn(console, 'warn'); + let createClass = React.createClass; + createClass = React.createClass; + expect(createClass).not.toBe(undefined); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'React.createClass is no longer supported. Use a plain ' + + "JavaScript class instead. If you're not yet ready to migrate, " + + 'create-react-class is available on npm as a temporary, ' + + 'drop-in replacement.', + ); + }); + + it('should warn once when attempting to access React.PropTypes', () => { + spyOn(console, 'warn'); + let PropTypes = React.PropTypes; + PropTypes = React.PropTypes; + expect(PropTypes).not.toBe(undefined); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'Warning: Accessing PropTypes via the main React package is ' + + 'deprecated, and will be removed in React v16.0. ' + + 'Use the prop-types package from npm instead. ' + + 'Version 15.5.10 provides a drop-in replacement. ' + + 'For info on usage, compatibility, migration and more, ' + + 'see https://fb.me/prop-types-docs', + ); + }); }); diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js deleted file mode 100644 index 3d4cf6654d61..000000000000 --- a/src/isomorphic/classic/class/ReactClass.js +++ /dev/null @@ -1,867 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactClass - */ - -'use strict'; - -var ReactComponent = require('ReactComponent'); -var ReactElement = require('ReactElement'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); -var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); - -var emptyObject = require('emptyObject'); -var invariant = require('invariant'); -var warning = require('warning'); - -import type {ReactPropTypeLocations} from 'ReactPropTypeLocations'; - -var MIXINS_KEY = 'mixins'; - -// Helper function to allow the creation of anonymous functions which do not -// have .name set to the name of the variable being assigned to. -function identity(fn) { - return fn; -} - -/** - * Policies that describe methods in `ReactClassInterface`. - */ -type SpecPolicy = - /** - * These methods may be defined only once by the class specification or mixin. - */ - | 'DEFINE_ONCE' /** - * These methods may be defined by both the class specification and mixins. - * Subsequent definitions will be chained. These methods must return void. - */ - | 'DEFINE_MANY' /** - * These methods are overriding the base class. - */ - | 'OVERRIDE_BASE' /** - * These methods are similar to DEFINE_MANY, except we assume they return - * objects. We try to merge the keys of the return values of all the mixed in - * functions. If there is a key conflict we throw. - */ - | 'DEFINE_MANY_MERGED'; - -var injectedMixins = []; - -/** - * Composite components are higher-level components that compose other composite - * or host components. - * - * To create a new type of `ReactClass`, pass a specification of - * your new class to `React.createClass`. The only requirement of your class - * specification is that you implement a `render` method. - * - * var MyComponent = React.createClass({ - * render: function() { - * return
Hello World
; - * } - * }); - * - * The class specification supports a specific protocol of methods that have - * special meaning (e.g. `render`). See `ReactClassInterface` for - * more the comprehensive protocol. Any other properties and methods in the - * class specification will be available on the prototype. - * - * @interface ReactClassInterface - * @internal - */ -var ReactClassInterface: {[key: string]: SpecPolicy} = { - /** - * An array of Mixin objects to include when defining your component. - * - * @type {array} - * @optional - */ - mixins: 'DEFINE_MANY', - - /** - * An object containing properties and methods that should be defined on - * the component's constructor instead of its prototype (static methods). - * - * @type {object} - * @optional - */ - statics: 'DEFINE_MANY', - - /** - * Definition of prop types for this component. - * - * @type {object} - * @optional - */ - propTypes: 'DEFINE_MANY', - - /** - * Definition of context types for this component. - * - * @type {object} - * @optional - */ - contextTypes: 'DEFINE_MANY', - - /** - * Definition of context types this component sets for its children. - * - * @type {object} - * @optional - */ - childContextTypes: 'DEFINE_MANY', - - // ==== Definition methods ==== - - /** - * Invoked when the component is mounted. Values in the mapping will be set on - * `this.props` if that prop is not specified (i.e. using an `in` check). - * - * This method is invoked before `getInitialState` and therefore cannot rely - * on `this.state` or use `this.setState`. - * - * @return {object} - * @optional - */ - getDefaultProps: 'DEFINE_MANY_MERGED', - - /** - * Invoked once before the component is mounted. The return value will be used - * as the initial value of `this.state`. - * - * getInitialState: function() { - * return { - * isOn: false, - * fooBaz: new BazFoo() - * } - * } - * - * @return {object} - * @optional - */ - getInitialState: 'DEFINE_MANY_MERGED', - - /** - * @return {object} - * @optional - */ - getChildContext: 'DEFINE_MANY_MERGED', - - /** - * Uses props from `this.props` and state from `this.state` to render the - * structure of the component. - * - * No guarantees are made about when or how often this method is invoked, so - * it must not have side effects. - * - * render: function() { - * var name = this.props.name; - * return
Hello, {name}!
; - * } - * - * @return {ReactComponent} - * @required - */ - render: 'DEFINE_ONCE', - - // ==== Delegate methods ==== - - /** - * Invoked when the component is initially created and about to be mounted. - * This may have side effects, but any external subscriptions or data created - * by this method must be cleaned up in `componentWillUnmount`. - * - * @optional - */ - componentWillMount: 'DEFINE_MANY', - - /** - * Invoked when the component has been mounted and has a DOM representation. - * However, there is no guarantee that the DOM node is in the document. - * - * Use this as an opportunity to operate on the DOM when the component has - * been mounted (initialized and rendered) for the first time. - * - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidMount: 'DEFINE_MANY', - - /** - * Invoked before the component receives new props. - * - * Use this as an opportunity to react to a prop transition by updating the - * state using `this.setState`. Current props are accessed via `this.props`. - * - * componentWillReceiveProps: function(nextProps, nextContext) { - * this.setState({ - * likesIncreasing: nextProps.likeCount > this.props.likeCount - * }); - * } - * - * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop - * transition may cause a state change, but the opposite is not true. If you - * need it, you are probably looking for `componentWillUpdate`. - * - * @param {object} nextProps - * @optional - */ - componentWillReceiveProps: 'DEFINE_MANY', - - /** - * Invoked while deciding if the component should be updated as a result of - * receiving new props, state and/or context. - * - * Use this as an opportunity to `return false` when you're certain that the - * transition to the new props/state/context will not require a component - * update. - * - * shouldComponentUpdate: function(nextProps, nextState, nextContext) { - * return !equal(nextProps, this.props) || - * !equal(nextState, this.state) || - * !equal(nextContext, this.context); - * } - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @return {boolean} True if the component should update. - * @optional - */ - shouldComponentUpdate: 'DEFINE_ONCE', - - /** - * Invoked when the component is about to update due to a transition from - * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` - * and `nextContext`. - * - * Use this as an opportunity to perform preparation before an update occurs. - * - * NOTE: You **cannot** use `this.setState()` in this method. - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @param {ReactReconcileTransaction} transaction - * @optional - */ - componentWillUpdate: 'DEFINE_MANY', - - /** - * Invoked when the component's DOM representation has been updated. - * - * Use this as an opportunity to operate on the DOM when the component has - * been updated. - * - * @param {object} prevProps - * @param {?object} prevState - * @param {?object} prevContext - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidUpdate: 'DEFINE_MANY', - - /** - * Invoked when the component is about to be removed from its parent and have - * its DOM representation destroyed. - * - * Use this as an opportunity to deallocate any external resources. - * - * NOTE: There is no `componentDidUnmount` since your component will have been - * destroyed by that point. - * - * @optional - */ - componentWillUnmount: 'DEFINE_MANY', - - // ==== Advanced methods ==== - - /** - * Updates the component's currently mounted DOM representation. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @internal - * @overridable - */ - updateComponent: 'OVERRIDE_BASE', -}; - -/** - * Mapping from class specification keys to special processing functions. - * - * Although these are declared like instance properties in the specification - * when defining classes using `React.createClass`, they are actually static - * and are accessible on the constructor instead of the prototype. Despite - * being static, they must be defined outside of the "statics" key under - * which all other static methods are defined. - */ -var RESERVED_SPEC_KEYS = { - displayName: function(Constructor, displayName) { - Constructor.displayName = displayName; - }, - mixins: function(Constructor, mixins) { - if (mixins) { - for (var i = 0; i < mixins.length; i++) { - mixSpecIntoComponent(Constructor, mixins[i]); - } - } - }, - childContextTypes: function(Constructor, childContextTypes) { - if (__DEV__) { - validateTypeDef(Constructor, childContextTypes, 'childContext'); - } - Constructor.childContextTypes = Object.assign( - {}, - Constructor.childContextTypes, - childContextTypes, - ); - }, - contextTypes: function(Constructor, contextTypes) { - if (__DEV__) { - validateTypeDef(Constructor, contextTypes, 'context'); - } - Constructor.contextTypes = Object.assign( - {}, - Constructor.contextTypes, - contextTypes, - ); - }, - /** - * Special case getDefaultProps which should move into statics but requires - * automatic merging. - */ - getDefaultProps: function(Constructor, getDefaultProps) { - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps = createMergedResultFunction( - Constructor.getDefaultProps, - getDefaultProps, - ); - } else { - Constructor.getDefaultProps = getDefaultProps; - } - }, - propTypes: function(Constructor, propTypes) { - if (__DEV__) { - validateTypeDef(Constructor, propTypes, 'prop'); - } - Constructor.propTypes = Object.assign({}, Constructor.propTypes, propTypes); - }, - statics: function(Constructor, statics) { - mixStaticSpecIntoComponent(Constructor, statics); - }, - autobind: function() {}, // noop -}; - -function validateTypeDef( - Constructor, - typeDef, - location: ReactPropTypeLocations, -) { - for (var propName in typeDef) { - if (typeDef.hasOwnProperty(propName)) { - // use a warning instead of an invariant so components - // don't show up in prod but only in __DEV__ - warning( - typeof typeDef[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually from ' + - 'React.PropTypes.', - Constructor.displayName || 'ReactClass', - ReactPropTypeLocationNames[location], - propName, - ); - } - } -} - -function validateMethodOverride(isAlreadyDefined, name) { - var specPolicy = ReactClassInterface.hasOwnProperty(name) - ? ReactClassInterface[name] - : null; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactClassMixin.hasOwnProperty(name)) { - invariant( - specPolicy === 'OVERRIDE_BASE', - 'ReactClassInterface: You are attempting to override ' + - '`%s` from your class specification. Ensure that your method names ' + - 'do not overlap with React methods.', - name, - ); - } - - // Disallow defining methods more than once unless explicitly allowed. - if (isAlreadyDefined) { - invariant( - specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED', - 'ReactClassInterface: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be due ' + - 'to a mixin.', - name, - ); - } -} - -/** - * Mixin helper which handles policy validation and reserved - * specification keys when building React classes. - */ -function mixSpecIntoComponent(Constructor, spec) { - if (!spec) { - if (__DEV__) { - var typeofSpec = typeof spec; - var isMixinValid = typeofSpec === 'object' && spec !== null; - - warning( - isMixinValid, - "%s: You're attempting to include a mixin that is either null " + - 'or not an object. Check the mixins included by the component, ' + - 'as well as any mixins they include themselves. ' + - 'Expected object but got %s.', - Constructor.displayName || 'ReactClass', - spec === null ? null : typeofSpec, - ); - } - - return; - } - - invariant( - typeof spec !== 'function', - "ReactClass: You're attempting to " + - 'use a component class or function as a mixin. Instead, just use a ' + - 'regular object.', - ); - invariant( - !ReactElement.isValidElement(spec), - "ReactClass: You're attempting to " + - 'use a component as a mixin. Instead, just use a regular object.', - ); - - var proto = Constructor.prototype; - var autoBindPairs = proto.__reactAutoBindPairs; - - // By handling mixins before any other properties, we ensure the same - // chaining order is applied to methods with DEFINE_MANY policy, whether - // mixins are listed before or after these methods in the spec. - if (spec.hasOwnProperty(MIXINS_KEY)) { - RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); - } - - for (var name in spec) { - if (!spec.hasOwnProperty(name)) { - continue; - } - - if (name === MIXINS_KEY) { - // We have already handled mixins in a special case above. - continue; - } - - var property = spec[name]; - var isAlreadyDefined = proto.hasOwnProperty(name); - validateMethodOverride(isAlreadyDefined, name); - - if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { - RESERVED_SPEC_KEYS[name](Constructor, property); - } else { - // Setup methods on prototype: - // The following member methods should not be automatically bound: - // 1. Expected ReactClass methods (in the "interface"). - // 2. Overridden methods (that were mixed in). - var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); - var isFunction = typeof property === 'function'; - var shouldAutoBind = - isFunction && - !isReactClassMethod && - !isAlreadyDefined && - spec.autobind !== false; - - if (shouldAutoBind) { - autoBindPairs.push(name, property); - proto[name] = property; - } else { - if (isAlreadyDefined) { - var specPolicy = ReactClassInterface[name]; - - // These cases should already be caught by validateMethodOverride. - invariant( - isReactClassMethod && - (specPolicy === 'DEFINE_MANY_MERGED' || - specPolicy === 'DEFINE_MANY'), - 'ReactClass: Unexpected spec policy %s for key %s ' + - 'when mixing in component specs.', - specPolicy, - name, - ); - - // For methods which are defined more than once, call the existing - // methods before calling the new property, merging if appropriate. - if (specPolicy === 'DEFINE_MANY_MERGED') { - proto[name] = createMergedResultFunction(proto[name], property); - } else if (specPolicy === 'DEFINE_MANY') { - proto[name] = createChainedFunction(proto[name], property); - } - } else { - proto[name] = property; - if (__DEV__) { - // Add verbose displayName to the function, which helps when looking - // at profiling tools. - if (typeof property === 'function' && spec.displayName) { - proto[name].displayName = spec.displayName + '_' + name; - } - } - } - } - } - } -} - -function mixStaticSpecIntoComponent(Constructor, statics) { - if (!statics) { - return; - } - for (var name in statics) { - var property = statics[name]; - if (!statics.hasOwnProperty(name)) { - continue; - } - - var isReserved = name in RESERVED_SPEC_KEYS; - invariant( - !isReserved, - 'ReactClass: You are attempting to define a reserved ' + - 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + - 'as an instance property instead; it will still be accessible on the ' + - 'constructor.', - name, - ); - - var isInherited = name in Constructor; - invariant( - !isInherited, - 'ReactClass: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be ' + - 'due to a mixin.', - name, - ); - Constructor[name] = property; - } -} - -/** - * Merge two objects, but throw if both contain the same key. - * - * @param {object} one The first object, which is mutated. - * @param {object} two The second object - * @return {object} one after it has been mutated to contain everything in two. - */ -function mergeIntoWithNoDuplicateKeys(one, two) { - invariant( - one && two && typeof one === 'object' && typeof two === 'object', - 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.', - ); - - for (var key in two) { - if (two.hasOwnProperty(key)) { - invariant( - one[key] === undefined, - 'mergeIntoWithNoDuplicateKeys(): ' + - 'Tried to merge two objects with the same key: `%s`. This conflict ' + - 'may be due to a mixin; in particular, this may be caused by two ' + - 'getInitialState() or getDefaultProps() methods returning objects ' + - 'with clashing keys.', - key, - ); - one[key] = two[key]; - } - } - return one; -} - -/** - * Creates a function that invokes two functions and merges their return values. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createMergedResultFunction(one, two) { - return function mergedResult() { - var a = one.apply(this, arguments); - var b = two.apply(this, arguments); - if (a == null) { - return b; - } else if (b == null) { - return a; - } - var c = {}; - mergeIntoWithNoDuplicateKeys(c, a); - mergeIntoWithNoDuplicateKeys(c, b); - return c; - }; -} - -/** - * Creates a function that invokes two functions and ignores their return vales. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createChainedFunction(one, two) { - return function chainedFunction() { - one.apply(this, arguments); - two.apply(this, arguments); - }; -} - -/** - * Binds a method to the component. - * - * @param {object} component Component whose method is going to be bound. - * @param {function} method Method to be bound. - * @return {function} The bound method. - */ -function bindAutoBindMethod(component, method) { - var boundMethod = method.bind(component); - if (__DEV__) { - boundMethod.__reactBoundContext = component; - boundMethod.__reactBoundMethod = method; - boundMethod.__reactBoundArguments = null; - var componentName = component.constructor.displayName; - var _bind = boundMethod.bind; - boundMethod.bind = function(newThis, ...args) { - // User is trying to bind() an autobound method; we effectively will - // ignore the value of "this" that the user is trying to use, so - // let's warn. - if (newThis !== component && newThis !== null) { - warning( - false, - 'bind(): React component methods may only be bound to the ' + - 'component instance. See %s', - componentName, - ); - } else if (!args.length) { - warning( - false, - 'bind(): You are binding a component method to the component. ' + - 'React does this for you automatically in a high-performance ' + - 'way, so you can safely remove this call. See %s', - componentName, - ); - return boundMethod; - } - var reboundMethod = _bind.apply(boundMethod, arguments); - reboundMethod.__reactBoundContext = component; - reboundMethod.__reactBoundMethod = method; - reboundMethod.__reactBoundArguments = args; - return reboundMethod; - }; - } - return boundMethod; -} - -/** - * Binds all auto-bound methods in a component. - * - * @param {object} component Component whose method is going to be bound. - */ -function bindAutoBindMethods(component) { - var pairs = component.__reactAutoBindPairs; - for (var i = 0; i < pairs.length; i += 2) { - var autoBindKey = pairs[i]; - var method = pairs[i + 1]; - component[autoBindKey] = bindAutoBindMethod(component, method); - } -} - -/** - * Add more to the ReactClass base class. These are all legacy features and - * therefore not already part of the modern ReactComponent. - */ -var ReactClassMixin = { - /** - * TODO: This will be deprecated because state should always keep a consistent - * type signature and the only use case for this, is to avoid that. - */ - replaceState: function(newState, callback) { - this.updater.enqueueReplaceState(this, newState); - if (callback) { - this.updater.enqueueCallback(this, callback, 'replaceState'); - } - }, - - /** - * Checks whether or not this composite component is mounted. - * @return {boolean} True if mounted, false otherwise. - * @protected - * @final - */ - isMounted: function() { - return this.updater.isMounted(this); - }, -}; - -var ReactClassComponent = function() {}; -Object.assign( - ReactClassComponent.prototype, - ReactComponent.prototype, - ReactClassMixin, -); - -let didWarnDeprecated = false; - -/** - * Module for creating composite components. - * - * @class ReactClass - */ -var ReactClass = { - /** - * Creates a composite component class given a class specification. - * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass - * - * @param {object} spec Class specification (which must define `render`). - * @return {function} Component constructor function. - * @public - */ - createClass: function(spec) { - if (__DEV__) { - warning( - didWarnDeprecated, - '%s: React.createClass is deprecated and will be removed in version 16. ' + - "Use plain JavaScript classes instead. If you're not yet ready to " + - 'migrate, create-react-class is available on npm as a ' + - 'drop-in replacement.', - (spec && spec.displayName) || 'A Component', - ); - didWarnDeprecated = true; - } - - // To keep our warnings more understandable, we'll use a little hack here to - // ensure that Constructor.name !== 'Constructor'. This makes sure we don't - // unnecessarily identify a class without displayName as 'Constructor'. - var Constructor = identity(function(props, context, updater) { - // This constructor gets overridden by mocks. The argument is used - // by mocks to assert on what gets mounted. - - if (__DEV__) { - warning( - this instanceof Constructor, - 'Something is calling a React component directly. Use a factory or ' + - 'JSX instead. See: https://fb.me/react-legacyfactory', - ); - } - - // Wire up auto-binding - if (this.__reactAutoBindPairs.length) { - bindAutoBindMethods(this); - } - - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - - this.state = null; - - // ReactClasses doesn't have constructors. Instead, they use the - // getInitialState and componentWillMount methods for initialization. - - var initialState = this.getInitialState ? this.getInitialState() : null; - if (__DEV__) { - // We allow auto-mocks to proceed as if they're returning null. - if ( - initialState === undefined && - this.getInitialState._isMockFunction - ) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - initialState = null; - } - } - invariant( - typeof initialState === 'object' && !Array.isArray(initialState), - '%s.getInitialState(): must return an object or null', - Constructor.displayName || 'ReactCompositeComponent', - ); - - this.state = initialState; - }); - Constructor.prototype = new ReactClassComponent(); - Constructor.prototype.constructor = Constructor; - Constructor.prototype.__reactAutoBindPairs = []; - - injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); - - mixSpecIntoComponent(Constructor, spec); - - // Initialize the defaultProps property after all mixins have been merged. - if (Constructor.getDefaultProps) { - Constructor.defaultProps = Constructor.getDefaultProps(); - } - - if (__DEV__) { - // This is a tag to indicate that the use of these method names is ok, - // since it's used with createClass. If it's not, then it's likely a - // mistake so we'll warn you to use the static property, property - // initializer or constructor respectively. - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps.isReactClassApproved = {}; - } - if (Constructor.prototype.getInitialState) { - Constructor.prototype.getInitialState.isReactClassApproved = {}; - } - } - - invariant( - Constructor.prototype.render, - 'createClass(...): Class specification must implement a `render` method.', - ); - - if (__DEV__) { - warning( - !Constructor.prototype.componentShouldUpdate, - '%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.', - spec.displayName || 'A component', - ); - warning( - !Constructor.prototype.componentWillRecieveProps, - '%s has a method called ' + - 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', - spec.displayName || 'A component', - ); - } - - // Reduce time spent doing lookups by setting these on the prototype. - for (var methodName in ReactClassInterface) { - if (!Constructor.prototype[methodName]) { - Constructor.prototype[methodName] = null; - } - } - - return Constructor; - }, - - injection: { - injectMixin: function(mixin) { - injectedMixins.push(mixin); - }, - }, -}; - -module.exports = ReactClass; diff --git a/src/isomorphic/classic/class/__tests__/ReactClass-test.js b/src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js similarity index 74% rename from src/isomorphic/classic/class/__tests__/ReactClass-test.js rename to src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js index 17fa24e6776b..4c89c732acf1 100644 --- a/src/isomorphic/classic/class/__tests__/ReactClass-test.js +++ b/src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js @@ -14,58 +14,32 @@ var React; var ReactDOM; var ReactTestUtils; -var PropTypes; +var createReactClass; -describe('ReactClass-spec', () => { +describe('create-react-class-integration', () => { beforeEach(() => { React = require('React'); ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); - PropTypes = require('prop-types'); - }); - - it('should warn on first call to React.createClass', () => { - spyOn(console, 'error'); - const spec = { - displayName: 'MyComponent', - render() { - return
; - }, - }; - React.createClass(spec); - React.createClass(spec); - expect(console.error.calls.count()).toEqual(1); - expect(console.error.calls.count()).toEqual(1); - expect(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: MyComponent: React.createClass is deprecated and will be removed in ' + - "version 16. Use plain JavaScript classes instead. If you're not yet " + - 'ready to migrate, create-react-class is available on npm as a ' + - 'drop-in replacement.', + var createReactClassFactory = require('create-react-class/factory'); + createReactClass = createReactClassFactory( + React.Component, + React.isValidElement, + require('ReactNoopUpdateQueue'), ); - console.error.calls.reset(); }); it('should throw when `render` is not specified', () => { expect(function() { - React.createClass({}); + createReactClass({}); }).toThrowError( 'createClass(...): Class specification must implement a `render` method.', ); }); - it('should copy `displayName` onto the Constructor', () => { - var TestComponent = React.createClass({ - render: function() { - return
; - }, - }); - - expect(TestComponent.displayName).toBe('TestComponent'); - }); - it('should copy prop types onto the Constructor', () => { var propValidator = jest.fn(); - var TestComponent = React.createClass({ + var TestComponent = createReactClass({ propTypes: { value: propValidator, }, @@ -80,7 +54,7 @@ describe('ReactClass-spec', () => { it('should warn on invalid prop types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', propTypes: { prop: null, @@ -98,7 +72,7 @@ describe('ReactClass-spec', () => { it('should warn on invalid context types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', contextTypes: { prop: null, @@ -116,7 +90,7 @@ describe('ReactClass-spec', () => { it('should throw on invalid child context types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', childContextTypes: { prop: null, @@ -135,7 +109,7 @@ describe('ReactClass-spec', () => { it('should warn when mispelling shouldComponentUpdate', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ componentShouldUpdate: function() { return false; }, @@ -150,7 +124,7 @@ describe('ReactClass-spec', () => { 'because the function is expected to return a value.', ); - React.createClass({ + createReactClass({ displayName: 'NamedComponent', componentShouldUpdate: function() { return false; @@ -169,7 +143,7 @@ describe('ReactClass-spec', () => { it('should warn when mispelling componentWillReceiveProps', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ componentWillRecieveProps: function() { return false; }, @@ -186,7 +160,7 @@ describe('ReactClass-spec', () => { it('should throw if a reserved property is in statics', () => { expect(function() { - React.createClass({ + createReactClass({ statics: { getDefaultProps: function() { return { @@ -211,16 +185,16 @@ describe('ReactClass-spec', () => { xit('should warn when using deprecated non-static spec keys', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ mixins: [{}], propTypes: { - foo: PropTypes.string, + foo: React.PropTypes.string, }, contextTypes: { - foo: PropTypes.string, + foo: React.PropTypes.string, }, childContextTypes: { - foo: PropTypes.string, + foo: React.PropTypes.string, }, render: function() { return
; @@ -246,7 +220,7 @@ describe('ReactClass-spec', () => { }); it('should support statics', () => { - var Component = React.createClass({ + var Component = createReactClass({ statics: { abc: 'def', def: 0, @@ -276,7 +250,7 @@ describe('ReactClass-spec', () => { }); it('should work with object getInitialState() return values', () => { - var Component = React.createClass({ + var Component = createReactClass({ getInitialState: function() { return { occupation: 'clown', @@ -292,9 +266,9 @@ describe('ReactClass-spec', () => { }); it('renders based on context getInitialState', () => { - var Foo = React.createClass({ + var Foo = createReactClass({ contextTypes: { - className: PropTypes.string, + className: React.PropTypes.string, }, getInitialState() { return {className: this.context.className}; @@ -304,9 +278,9 @@ describe('ReactClass-spec', () => { }, }); - var Outer = React.createClass({ + var Outer = createReactClass({ childContextTypes: { - className: PropTypes.string, + className: React.PropTypes.string, }, getChildContext() { return {className: 'foo'}; @@ -323,7 +297,7 @@ describe('ReactClass-spec', () => { it('should throw with non-object getInitialState() return values', () => { [['an array'], 'a string', 1234].forEach(function(state) { - var Component = React.createClass({ + var Component = createReactClass({ getInitialState: function() { return state; }, @@ -341,7 +315,7 @@ describe('ReactClass-spec', () => { }); it('should work with a null getInitialState() return value', () => { - var Component = React.createClass({ + var Component = createReactClass({ getInitialState: function() { return null; }, @@ -356,7 +330,7 @@ describe('ReactClass-spec', () => { it('should throw when using legacy factories', () => { spyOn(console, 'error'); - var Component = React.createClass({ + var Component = createReactClass({ render() { return
; }, @@ -369,4 +343,84 @@ describe('ReactClass-spec', () => { 'factory or JSX instead. See: https://fb.me/react-legacyfactory', ); }); + + it('replaceState and callback works', () => { + var ops = []; + var Component = createReactClass({ + getInitialState() { + return {step: 0}; + }, + render() { + ops.push('Render: ' + this.state.step); + return
; + }, + }); + + var instance = ReactTestUtils.renderIntoDocument(); + instance.replaceState({step: 1}, () => { + ops.push('Callback: ' + instance.state.step); + }); + expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']); + }); + + it('isMounted works', () => { + spyOn(console, 'error'); + + var ops = []; + var instance; + var Component = createReactClass({ + displayName: 'MyComponent', + log(name) { + ops.push(`${name}: ${this.isMounted()}`); + }, + getInitialState() { + this.log('getInitialState'); + return {}; + }, + componentWillMount() { + this.log('componentWillMount'); + }, + componentDidMount() { + this.log('componentDidMount'); + }, + componentWillUpdate() { + this.log('componentWillUpdate'); + }, + componentDidUpdate() { + this.log('componentDidUpdate'); + }, + componentWillUnmount() { + this.log('componentWillUnmount'); + }, + render() { + instance = this; + this.log('render'); + return
; + }, + }); + + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + ReactDOM.unmountComponentAtNode(container); + instance.log('after unmount'); + expect(ops).toEqual([ + 'getInitialState: false', + 'componentWillMount: false', + 'render: false', + 'componentDidMount: true', + 'componentWillUpdate: true', + 'render: true', + 'componentDidUpdate: true', + 'componentWillUnmount: false', + 'after unmount: false', + ]); + + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toEqual( + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.', + ); + }); }); diff --git a/src/isomorphic/classic/class/createClass.js b/src/isomorphic/classic/class/createClass.js new file mode 100644 index 000000000000..3791e5392dc0 --- /dev/null +++ b/src/isomorphic/classic/class/createClass.js @@ -0,0 +1,19 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule createClass + */ + +'use strict'; + +var {Component} = require('ReactBaseClasses'); +var {isValidElement} = require('ReactElement'); +var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); +var factory = require('create-react-class/factory'); + +module.exports = factory(Component, isValidElement, ReactNoopUpdateQueue); diff --git a/src/isomorphic/modern/class/ReactComponent.js b/src/isomorphic/modern/class/ReactBaseClasses.js similarity index 83% rename from src/isomorphic/modern/class/ReactComponent.js rename to src/isomorphic/modern/class/ReactBaseClasses.js index aa03ca82045c..ed8d31e9bc33 100644 --- a/src/isomorphic/modern/class/ReactComponent.js +++ b/src/isomorphic/modern/class/ReactBaseClasses.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @providesModule ReactComponent + * @providesModule ReactBaseClasses */ 'use strict'; @@ -132,4 +132,28 @@ if (__DEV__) { } } -module.exports = ReactComponent; +/** + * Base class helpers for the updating state of a component. + */ +function ReactPureComponent(props, context, updater) { + // Duplicated from ReactComponent. + this.props = props; + this.context = context; + this.refs = emptyObject; + // We initialize the default updater but the real one gets injected by the + // renderer. + this.updater = updater || ReactNoopUpdateQueue; +} + +function ComponentDummy() {} +ComponentDummy.prototype = ReactComponent.prototype; +ReactPureComponent.prototype = new ComponentDummy(); +ReactPureComponent.prototype.constructor = ReactPureComponent; +// Avoid an extra prototype jump for these methods. +Object.assign(ReactPureComponent.prototype, ReactComponent.prototype); +ReactPureComponent.prototype.isPureReactComponent = true; + +module.exports = { + Component: ReactComponent, + PureComponent: ReactPureComponent, +}; diff --git a/src/isomorphic/modern/class/ReactPureComponent.js b/src/isomorphic/modern/class/ReactPureComponent.js deleted file mode 100644 index b4a808864b9c..000000000000 --- a/src/isomorphic/modern/class/ReactPureComponent.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactPureComponent - */ - -'use strict'; - -var ReactComponent = require('ReactComponent'); -var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); - -var emptyObject = require('emptyObject'); - -/** - * Base class helpers for the updating state of a component. - */ -function ReactPureComponent(props, context, updater) { - // Duplicated from ReactComponent. - this.props = props; - this.context = context; - this.refs = emptyObject; - // We initialize the default updater but the real one gets injected by the - // renderer. - this.updater = updater || ReactNoopUpdateQueue; -} - -function ComponentDummy() {} -ComponentDummy.prototype = ReactComponent.prototype; -ReactPureComponent.prototype = new ComponentDummy(); -ReactPureComponent.prototype.constructor = ReactPureComponent; -// Avoid an extra prototype jump for these methods. -Object.assign(ReactPureComponent.prototype, ReactComponent.prototype); -ReactPureComponent.prototype.isPureReactComponent = true; - -module.exports = ReactPureComponent; diff --git a/src/renderers/art/ReactART.js b/src/renderers/art/ReactART.js index b2d977a3ae56..7550e330d29e 100644 --- a/src/renderers/art/ReactART.js +++ b/src/renderers/art/ReactART.js @@ -24,6 +24,7 @@ const ReactInstanceMap = require('ReactInstanceMap'); const ReactMultiChild = require('ReactMultiChild'); const ReactUpdates = require('ReactUpdates'); +const createReactClass = require('createClass'); const emptyObject = require('emptyObject'); const invariant = require('invariant'); @@ -171,7 +172,7 @@ const ContainerMixin = assign({}, ReactMultiChild.Mixin, { // Surface is a React DOM Component, not an ART component. It serves as the // entry point into the ART reconciler. -const Surface = React.createClass({ +const Surface = createReactClass({ displayName: 'Surface', mixins: [ContainerMixin], diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js index 59eaec42bfc6..03dbda5dcc40 100644 --- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js @@ -1089,22 +1089,20 @@ describe('ReactDOMInput', () => { describe('assigning the value attribute on controlled inputs', function() { function getTestInput() { - return React.createClass({ - getInitialState: function() { - return { - value: this.props.value == null ? '' : this.props.value, - }; - }, - onChange: function(event) { + return class extends React.Component { + state = { + value: this.props.value == null ? '' : this.props.value, + }; + onChange = event => { this.setState({value: event.target.value}); - }, - render: function() { + }; + render() { var type = this.props.type; var value = this.state.value; return ; - }, - }); + } + }; } it('always sets the attribute when values change on text inputs', function() { diff --git a/yarn.lock b/yarn.lock index 4ae6507b79e6..8470365cae98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1449,11 +1449,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2: create-hash "^1.1.0" inherits "^2.0.1" -create-react-class@15.5.0: - version "15.5.0" - resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.0.tgz#7508ffcad56a0804fb244d6ff70b07648abfe5fb" +create-react-class@^15.5.2: + version "15.5.3" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.3.tgz#fb0f7cae79339e9a179e194ef466efa3923820fe" dependencies: fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" cross-spawn-async@^2.2.2: version "2.2.5" @@ -3713,7 +3715,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: