Skip to content

Commit

Permalink
Use React.autoBind by default.
Browse files Browse the repository at this point in the history
Per our discussion - this is the general approach we'd like to take for
the public facing API.

    var MyComponent = React.createClass({
      render: function() {
        return <div onClick={this.myCallback} />;
      },
      myCallback: function() {
      }
    });
  • Loading branch information
jeffmo authored and zpao committed Jun 24, 2013
1 parent 336a0fa commit c9ecbac
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 195 deletions.
217 changes: 112 additions & 105 deletions src/core/ReactCompositeComponent.js
Expand Up @@ -28,6 +28,17 @@ var keyMirror = require('keyMirror');
var merge = require('merge');
var mixInto = require('mixInto');

var invokedBeforeMount = function() {
invariant(
false,
'You have invoked a method that is automatically bound, before the ' +
'instance has been mounted. There is nothing conceptually wrong with ' +
'this, but since this method will be replaced with a new version once ' +
'the component is mounted - you should be aware of the fact that this ' +
'method will soon be replaced.'
);
};

/**
* Policies that describe methods in `ReactCompositeComponentInterface`.
*/
Expand Down Expand Up @@ -283,65 +294,92 @@ var RESERVED_SPEC_KEYS = {
}
};

function validateMethodOverride(proto, name) {
var specPolicy = ReactCompositeComponentInterface[name];

// Disallow overriding of base class methods unless explicitly allowed.
if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
invariant(
specPolicy === SpecPolicy.OVERRIDE_BASE,
'ReactCompositeComponentInterface: 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 (proto.hasOwnProperty(name)) {
invariant(
specPolicy === SpecPolicy.DEFINE_MANY,
'ReactCompositeComponentInterface: You are attempting to define ' +
'`%s` on your component more than once. This conflict may be due ' +
'to a mixin.',
name
);
}
}


function validateLifeCycleOnReplaceState(instance) {
var compositeLifeCycleState = instance._compositeLifeCycleState;
invariant(
instance.isMounted(),
'replaceState(...): Can only update a mounted component.'
);
invariant(
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
'replaceState(...): Cannot update while unmounting component or during ' +
'an existing state transition (such as within `render`).'
);
}

/**
* Custom version of `mixInto` which handles policy validation and reserved
* specification keys when building `ReactCompositeComponent` classses.
*/
function mixSpecIntoComponent(Constructor, spec) {
var proto = Constructor.prototype;
for (var name in spec) {
if (!spec.hasOwnProperty(name)) {
continue;
}
var property = spec[name];
var specPolicy = ReactCompositeComponentInterface[name];

// Disallow overriding of base class methods unless explicitly allowed.
if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
invariant(
specPolicy === SpecPolicy.OVERRIDE_BASE,
'ReactCompositeComponentInterface: You are attempting to override ' +
'`%s` from your class specification. Ensure that your method names ' +
'do not overlap with React methods.',
name
);
}

// Disallow using `React.autoBind` on internal methods.
if (specPolicy != null) {
invariant(
!property || !property.__reactAutoBind,
'ReactCompositeComponentInterface: You are attempting to use ' +
'`React.autoBind` on `%s`, a method that is internal to React.' +
'Internal methods are called with the component as the context.',
name
);
}

// Disallow defining methods more than once unless explicitly allowed.
if (proto.hasOwnProperty(name)) {
invariant(
specPolicy === SpecPolicy.DEFINE_MANY,
'ReactCompositeComponentInterface: You are attempting to define ' +
'`%s` on your component more than once. This conflict may be due ' +
'to a mixin.',
name
);
if (!spec.hasOwnProperty(name) || !property) {
continue;
}
validateMethodOverride(proto, name);

if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
RESERVED_SPEC_KEYS[name](Constructor, property);
} else if (property && property.__reactAutoBind) {
if (!proto.__reactAutoBindMap) {
proto.__reactAutoBindMap = {};
}
proto.__reactAutoBindMap[name] = property.__reactAutoBind;
} else if (proto.hasOwnProperty(name)) {
// For methods which are defined more than once, call the existing methods
// before calling the new property.
proto[name] = createChainedFunction(proto[name], property);
} else {
proto[name] = property;
// Setup methods on prototype:
// The following member methods should not be automatically bound:
// 1. Expected ReactCompositeComponent methods (in the "interface").
// 2. Overridden methods (that were mixed in).
var isCompositeComponentMethod = name in ReactCompositeComponentInterface;
var isInherited = name in proto;
var markedDontBind = property.__reactDontBind;
var isFunction = typeof property === 'function';
var shouldAutoBind =
isFunction &&
!isCompositeComponentMethod &&
!isInherited &&
!markedDontBind;

if (shouldAutoBind) {
if (!proto.__reactAutoBindMap) {
proto.__reactAutoBindMap = {};
}
proto.__reactAutoBindMap[name] = property;
proto[name] = invokedBeforeMount;
} else {
if (isInherited) {
// For methods which are defined more than once, call the existing
// methods before calling the new property.
proto[name] = createChainedFunction(proto[name], property);
} else {
proto[name] = property;
}
}
}
}
}
Expand Down Expand Up @@ -370,7 +408,26 @@ function createChainedFunction(one, two) {
* `this._compositeLifeCycleState` (which can be null).
*
* This is different from the life cycle state maintained by `ReactComponent` in
* `this._lifeCycleState`.
* `this._lifeCycleState`. The following diagram shows how the states overlap in
* time. There are times when the CompositeLifeCycle is null - at those times it
* is only meaningful to look at ComponentLifeCycle alone.
*
* Top Row: ReactComponent.ComponentLifeCycle
* Low Row: ReactComponent.CompositeLifeCycle
*
* +-------+------------------------------------------------------+--------+
* | UN | MOUNTED | UN |
* |MOUNTED| | MOUNTED|
* +-------+------------------------------------------------------+--------+
* | ^--------+ +------+ +------+ +------+ +--------^ |
* | | | | | | | | | | | |
* | 0--|MOUNTING|-0-|RECEIV|-0-|RECEIV|-0-|RECEIV|-0-| UN |--->0 |
* | | | |PROPS | | PROPS| | STATE| |MOUNTING| |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +--------+ +------+ +------+ +------+ +--------+ |
* | | | |
* +-------+------------------------------------------------------+--------+
*/
var CompositeLifeCycle = keyMirror({
/**
Expand Down Expand Up @@ -429,11 +486,7 @@ var ReactCompositeComponentMixin = {
*/
mountComponent: function(rootID, transaction) {
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);

// Unset `this._lifeCycleState` until after this method is finished.
this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED;
this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;

this._processProps(this.props);

if (this.__reactAutoBindMap) {
Expand All @@ -459,15 +512,11 @@ var ReactCompositeComponentMixin = {

// Done with mounting, `setState` will now trigger UI changes.
this._compositeLifeCycleState = null;
this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED;

var html = this._renderedComponent.mountComponent(rootID, transaction);

var markup = this._renderedComponent.mountComponent(rootID, transaction);
if (this.componentDidMount) {
transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
}

return html;
return markup;
},

/**
Expand Down Expand Up @@ -551,18 +600,7 @@ var ReactCompositeComponentMixin = {
*/
replaceState: function(completeState) {
var compositeLifeCycleState = this._compositeLifeCycleState;
invariant(
this.isMounted() ||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
'replaceState(...): Can only update a mounted (or mounting) component.'
);
invariant(
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
'replaceState(...): Cannot update while unmounting component or during ' +
'an existing state transition (such as within `render`).'
);

validateLifeCycleOnReplaceState.call(null, this);
this._pendingState = completeState;

// Do not trigger a state transition if we are in the middle of mounting or
Expand All @@ -583,7 +621,6 @@ var ReactCompositeComponentMixin = {
transaction
);
ReactComponent.ReactReconcileTransaction.release(transaction);

this._compositeLifeCycleState = null;
}
},
Expand Down Expand Up @@ -777,25 +814,12 @@ var ReactCompositeComponentMixin = {
*/
_bindAutoBindMethod: function(method) {
var component = this;
var hasWarned = false;
function autoBound(a, b, c, d, e, tooMany) {
invariant(
typeof tooMany === 'undefined',
'React.autoBind(...): Methods can only take a maximum of 5 arguments.'
);
if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) {
return method.call(component, a, b, c, d, e);
} else if (!hasWarned) {
hasWarned = true;
if (__DEV__) {
console.warn(
'React.autoBind(...): Attempted to invoke an auto-bound method ' +
'on an unmounted instance of `%s`. You either have a memory leak ' +
'or an event handler that is being run after unmounting.',
component.constructor.displayName || 'ReactCompositeComponent'
);
}
}
return method.call(component, a, b, c, d, e);
}
return autoBound;
}
Expand Down Expand Up @@ -850,33 +874,16 @@ var ReactCompositeComponent = {
},

/**
* Marks the provided method to be automatically bound to the component.
* This means the method's context will always be the component.
*
* React.createClass({
* handleClick: React.autoBind(function() {
* this.setState({jumping: true});
* }),
* render: function() {
* return <a onClick={this.handleClick}>Jump</a>;
* }
* });
* TODO: Delete this when all callers have been updated to rely on this
* behavior being the default.
*
* Backwards compatible stub for what is now the default behavior.
* @param {function} method Method to be bound.
* @public
*/
autoBind: function(method) {
function unbound() {
invariant(
false,
'React.autoBind(...): Attempted to invoke an auto-bound method that ' +
'was not correctly defined on the class specification.'
);
}
unbound.__reactAutoBind = method;
return unbound;
return method;
}

};

module.exports = ReactCompositeComponent;
48 changes: 48 additions & 0 deletions src/core/ReactDoNotBindDeprecated.js
@@ -0,0 +1,48 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactDoNotBindDeprecated
*/

var ReactDoNotBindDeprecated = {
/**
* Marks the method for not being automatically bound on component mounting. A
* couple of reasons you might want to use this:
*
* - Automatically supporting the previous behavior in components that were
* built with previous versions of React.
* - Tuning performance, by avoiding binding on initial render for methods
* that are always invoked while being preceded by `this.`. Such binds are
* unnecessary.
*
* React.createClass({
* handleClick: ReactDoNotBindDeprecated.doNotBind(function() {
* alert(this.setState); // undefined!
* }),
* render: function() {
* return <a onClick={this.handleClick}>Jump</a>;
* }
* });
*
* @param {function} method Method to avoid automatically binding.
* @public
*/
doNotBind: function(method) {
method.__reactDontBind = true; // Mutating
return method;
}
};

module.exports = ReactDoNotBindDeprecated;
2 changes: 1 addition & 1 deletion src/core/ReactID.js
Expand Up @@ -30,7 +30,7 @@ var nodeCache = {};
* other objects so just return '' if we're given something other than a
* DOM node (such as window).
*
* @param {DOMElement|DOMWindow|DOMDocument} node DOM node.
* @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node.
* @returns {string} ID of the supplied `domNode`.
*/
function getID(node) {
Expand Down

0 comments on commit c9ecbac

Please sign in to comment.