mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Breaking changes - key/ref are no longer accessible on props but they are accessible on the descriptors. This means that parents/owners can access it but not the component itself. - Descriptor factories are now plain functions and you can't rely on the prototype or constructors of descriptors to identify the component type. Existing descriptor factories are now wrapped in a legacy factory. Currently it does nothing but it will give us a hook to track callers to factories that are not using JSX but just invoking the function directly. It also proxies static methods/properties to the underlying class. The newer factories don't have this feature. ReactTextComponent has it's own little factory because it's props is not an object. This is a detail and will go away once ReactTextComponent no longer needs descriptors.
447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
/**
|
|
* Copyright 2013-2014 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 ReactComponent
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var ReactDescriptor = require('ReactDescriptor');
|
|
var ReactOwner = require('ReactOwner');
|
|
var ReactUpdates = require('ReactUpdates');
|
|
|
|
var invariant = require('invariant');
|
|
var keyMirror = require('keyMirror');
|
|
var merge = require('merge');
|
|
|
|
/**
|
|
* Every React component is in one of these life cycles.
|
|
*/
|
|
var ComponentLifeCycle = keyMirror({
|
|
/**
|
|
* Mounted components have a DOM node representation and are capable of
|
|
* receiving new props.
|
|
*/
|
|
MOUNTED: null,
|
|
/**
|
|
* Unmounted components are inactive and cannot receive new props.
|
|
*/
|
|
UNMOUNTED: null
|
|
});
|
|
|
|
var injected = false;
|
|
|
|
/**
|
|
* Optionally injectable environment dependent cleanup hook. (server vs.
|
|
* browser etc). Example: A browser system caches DOM nodes based on component
|
|
* ID and must remove that cache entry when this instance is unmounted.
|
|
*
|
|
* @private
|
|
*/
|
|
var unmountIDFromEnvironment = null;
|
|
|
|
/**
|
|
* The "image" of a component tree, is the platform specific (typically
|
|
* serialized) data that represents a tree of lower level UI building blocks.
|
|
* On the web, this "image" is HTML markup which describes a construction of
|
|
* low level `div` and `span` nodes. Other platforms may have different
|
|
* encoding of this "image". This must be injected.
|
|
*
|
|
* @private
|
|
*/
|
|
var mountImageIntoNode = null;
|
|
|
|
/**
|
|
* Components are the basic units of composition in React.
|
|
*
|
|
* Every component accepts a set of keyed input parameters known as "props" that
|
|
* are initialized by the constructor. Once a component is mounted, the props
|
|
* can be mutated using `setProps` or `replaceProps`.
|
|
*
|
|
* Every component is capable of the following operations:
|
|
*
|
|
* `mountComponent`
|
|
* Initializes the component, renders markup, and registers event listeners.
|
|
*
|
|
* `receiveComponent`
|
|
* Updates the rendered DOM nodes to match the given component.
|
|
*
|
|
* `unmountComponent`
|
|
* Releases any resources allocated by this component.
|
|
*
|
|
* Components can also be "owned" by other components. Being owned by another
|
|
* component means being constructed by that component. This is different from
|
|
* being the child of a component, which means having a DOM representation that
|
|
* is a child of the DOM representation of that component.
|
|
*
|
|
* @class ReactComponent
|
|
*/
|
|
var ReactComponent = {
|
|
|
|
injection: {
|
|
injectEnvironment: function(ReactComponentEnvironment) {
|
|
invariant(
|
|
!injected,
|
|
'ReactComponent: injectEnvironment() can only be called once.'
|
|
);
|
|
mountImageIntoNode = ReactComponentEnvironment.mountImageIntoNode;
|
|
unmountIDFromEnvironment =
|
|
ReactComponentEnvironment.unmountIDFromEnvironment;
|
|
ReactComponent.BackendIDOperations =
|
|
ReactComponentEnvironment.BackendIDOperations;
|
|
injected = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
LifeCycle: ComponentLifeCycle,
|
|
|
|
/**
|
|
* Injected module that provides ability to mutate individual properties.
|
|
* Injected into the base class because many different subclasses need access
|
|
* to this.
|
|
*
|
|
* @internal
|
|
*/
|
|
BackendIDOperations: null,
|
|
|
|
/**
|
|
* Base functionality for every ReactComponent constructor. Mixed into the
|
|
* `ReactComponent` prototype, but exposed statically for easy access.
|
|
*
|
|
* @lends {ReactComponent.prototype}
|
|
*/
|
|
Mixin: {
|
|
|
|
/**
|
|
* Checks whether or not this component is mounted.
|
|
*
|
|
* @return {boolean} True if mounted, false otherwise.
|
|
* @final
|
|
* @protected
|
|
*/
|
|
isMounted: function() {
|
|
return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
|
|
},
|
|
|
|
/**
|
|
* Sets a subset of the props.
|
|
*
|
|
* @param {object} partialProps Subset of the next props.
|
|
* @param {?function} callback Called after props are updated.
|
|
* @final
|
|
* @public
|
|
*/
|
|
setProps: function(partialProps, callback) {
|
|
// Merge with the pending descriptor if it exists, otherwise with existing
|
|
// descriptor props.
|
|
var descriptor = this._pendingDescriptor || this._descriptor;
|
|
this.replaceProps(
|
|
merge(descriptor.props, partialProps),
|
|
callback
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Replaces all of the props.
|
|
*
|
|
* @param {object} props New props.
|
|
* @param {?function} callback Called after props are updated.
|
|
* @final
|
|
* @public
|
|
*/
|
|
replaceProps: function(props, callback) {
|
|
invariant(
|
|
this.isMounted(),
|
|
'replaceProps(...): Can only update a mounted component.'
|
|
);
|
|
invariant(
|
|
this._mountDepth === 0,
|
|
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
|
|
'component with a parent. This is an anti-pattern since props will ' +
|
|
'get reactively updated when rendered. Instead, change the owner\'s ' +
|
|
'`render` method to pass the correct value as props to the component ' +
|
|
'where it is created.'
|
|
);
|
|
// This is a deoptimized path. We optimize for always having a descriptor.
|
|
// This creates an extra internal descriptor.
|
|
this._pendingDescriptor = ReactDescriptor.cloneAndReplaceProps(
|
|
this._pendingDescriptor || this._descriptor,
|
|
props
|
|
);
|
|
ReactUpdates.enqueueUpdate(this, callback);
|
|
},
|
|
|
|
/**
|
|
* Schedule a partial update to the props. Only used for internal testing.
|
|
*
|
|
* @param {object} partialProps Subset of the next props.
|
|
* @param {?function} callback Called after props are updated.
|
|
* @final
|
|
* @internal
|
|
*/
|
|
_setPropsInternal: function(partialProps, callback) {
|
|
// This is a deoptimized path. We optimize for always having a descriptor.
|
|
// This creates an extra internal descriptor.
|
|
var descriptor = this._pendingDescriptor || this._descriptor;
|
|
this._pendingDescriptor = ReactDescriptor.cloneAndReplaceProps(
|
|
descriptor,
|
|
merge(descriptor.props, partialProps)
|
|
);
|
|
ReactUpdates.enqueueUpdate(this, callback);
|
|
},
|
|
|
|
/**
|
|
* Base constructor for all React components.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.construct.call(this, ...)`.
|
|
*
|
|
* @param {ReactDescriptor} descriptor
|
|
* @internal
|
|
*/
|
|
construct: function(descriptor) {
|
|
// This is the public exposed props object after it has been processed
|
|
// with default props. The descriptor's props represents the true internal
|
|
// state of the props.
|
|
this.props = descriptor.props;
|
|
// Record the component responsible for creating this component.
|
|
// This is accessible through the descriptor but we maintain an extra
|
|
// field for compatibility with devtools and as a way to make an
|
|
// incremental update. TODO: Consider deprecating this field.
|
|
this._owner = descriptor._owner;
|
|
|
|
// All components start unmounted.
|
|
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
|
|
|
// See ReactUpdates.
|
|
this._pendingCallbacks = null;
|
|
|
|
// We keep the old descriptor and a reference to the pending descriptor
|
|
// to track updates.
|
|
this._descriptor = descriptor;
|
|
this._pendingDescriptor = null;
|
|
},
|
|
|
|
/**
|
|
* Initializes the component, renders markup, and registers event listeners.
|
|
*
|
|
* NOTE: This does not insert any nodes into the DOM.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.mountComponent.call(this, ...)`.
|
|
*
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @param {number} mountDepth number of components in the owner hierarchy.
|
|
* @return {?string} Rendered markup to be inserted into the DOM.
|
|
* @internal
|
|
*/
|
|
mountComponent: function(rootID, transaction, mountDepth) {
|
|
invariant(
|
|
!this.isMounted(),
|
|
'mountComponent(%s, ...): Can only mount an unmounted component. ' +
|
|
'Make sure to avoid storing components between renders or reusing a ' +
|
|
'single component instance in multiple places.',
|
|
rootID
|
|
);
|
|
var ref = this._descriptor.ref;
|
|
if (ref != null) {
|
|
var owner = this._descriptor._owner;
|
|
ReactOwner.addComponentAsRefTo(this, ref, owner);
|
|
}
|
|
this._rootNodeID = rootID;
|
|
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
|
|
this._mountDepth = mountDepth;
|
|
// Effectively: return '';
|
|
},
|
|
|
|
/**
|
|
* Releases any resources allocated by `mountComponent`.
|
|
*
|
|
* NOTE: This does not remove any nodes from the DOM.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.unmountComponent.call(this)`.
|
|
*
|
|
* @internal
|
|
*/
|
|
unmountComponent: function() {
|
|
invariant(
|
|
this.isMounted(),
|
|
'unmountComponent(): Can only unmount a mounted component.'
|
|
);
|
|
var ref = this._descriptor.ref;
|
|
if (ref != null) {
|
|
ReactOwner.removeComponentAsRefFrom(this, ref, this._owner);
|
|
}
|
|
unmountIDFromEnvironment(this._rootNodeID);
|
|
this._rootNodeID = null;
|
|
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
|
},
|
|
|
|
/**
|
|
* Given a new instance of this component, updates the rendered DOM nodes
|
|
* as if that instance was rendered instead.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.receiveComponent.call(this, ...)`.
|
|
*
|
|
* @param {object} nextComponent Next set of properties.
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @internal
|
|
*/
|
|
receiveComponent: function(nextDescriptor, transaction) {
|
|
invariant(
|
|
this.isMounted(),
|
|
'receiveComponent(...): Can only update a mounted component.'
|
|
);
|
|
this._pendingDescriptor = nextDescriptor;
|
|
this.performUpdateIfNecessary(transaction);
|
|
},
|
|
|
|
/**
|
|
* If `_pendingDescriptor` is set, update the component.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @internal
|
|
*/
|
|
performUpdateIfNecessary: function(transaction) {
|
|
if (this._pendingDescriptor == null) {
|
|
return;
|
|
}
|
|
var prevDescriptor = this._descriptor;
|
|
var nextDescriptor = this._pendingDescriptor;
|
|
this._descriptor = nextDescriptor;
|
|
this.props = nextDescriptor.props;
|
|
this._owner = nextDescriptor._owner;
|
|
this._pendingDescriptor = null;
|
|
this.updateComponent(transaction, prevDescriptor);
|
|
},
|
|
|
|
/**
|
|
* Updates the component's currently mounted representation.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {object} prevDescriptor
|
|
* @internal
|
|
*/
|
|
updateComponent: function(transaction, prevDescriptor) {
|
|
var nextDescriptor = this._descriptor;
|
|
|
|
// If either the owner or a `ref` has changed, make sure the newest owner
|
|
// has stored a reference to `this`, and the previous owner (if different)
|
|
// has forgotten the reference to `this`. We use the descriptor instead
|
|
// of the public this.props because the post processing cannot determine
|
|
// a ref. The ref conceptually lives on the descriptor.
|
|
|
|
// TODO: Should this even be possible? The owner cannot change because
|
|
// it's forbidden by shouldUpdateReactComponent. The ref can change
|
|
// if you swap the keys of but not the refs. Reconsider where this check
|
|
// is made. It probably belongs where the key checking and
|
|
// instantiateReactComponent is done.
|
|
|
|
if (nextDescriptor._owner !== prevDescriptor._owner ||
|
|
nextDescriptor.ref !== prevDescriptor.ref) {
|
|
if (prevDescriptor.ref != null) {
|
|
ReactOwner.removeComponentAsRefFrom(
|
|
this, prevDescriptor.ref, prevDescriptor._owner
|
|
);
|
|
}
|
|
// Correct, even if the owner is the same, and only the ref has changed.
|
|
if (nextDescriptor.ref != null) {
|
|
ReactOwner.addComponentAsRefTo(
|
|
this,
|
|
nextDescriptor.ref,
|
|
nextDescriptor._owner
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mounts this component and inserts it into the DOM.
|
|
*
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {DOMElement} container DOM element to mount into.
|
|
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
|
* @final
|
|
* @internal
|
|
* @see {ReactMount.renderComponent}
|
|
*/
|
|
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
|
|
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
|
|
transaction.perform(
|
|
this._mountComponentIntoNode,
|
|
this,
|
|
rootID,
|
|
container,
|
|
transaction,
|
|
shouldReuseMarkup
|
|
);
|
|
ReactUpdates.ReactReconcileTransaction.release(transaction);
|
|
},
|
|
|
|
/**
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {DOMElement} container DOM element to mount into.
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
|
* @final
|
|
* @private
|
|
*/
|
|
_mountComponentIntoNode: function(
|
|
rootID,
|
|
container,
|
|
transaction,
|
|
shouldReuseMarkup) {
|
|
var markup = this.mountComponent(rootID, transaction, 0);
|
|
mountImageIntoNode(markup, container, shouldReuseMarkup);
|
|
},
|
|
|
|
/**
|
|
* Checks if this component is owned by the supplied `owner` component.
|
|
*
|
|
* @param {ReactComponent} owner Component to check.
|
|
* @return {boolean} True if `owners` owns this component.
|
|
* @final
|
|
* @internal
|
|
*/
|
|
isOwnedBy: function(owner) {
|
|
return this._owner === owner;
|
|
},
|
|
|
|
/**
|
|
* Gets another component, that shares the same owner as this one, by ref.
|
|
*
|
|
* @param {string} ref of a sibling Component.
|
|
* @return {?ReactComponent} the actual sibling Component.
|
|
* @final
|
|
* @internal
|
|
*/
|
|
getSiblingByRef: function(ref) {
|
|
var owner = this._owner;
|
|
if (!owner || !owner.refs) {
|
|
return null;
|
|
}
|
|
return owner.refs[ref];
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = ReactComponent;
|