Refactor ReactComponent to have no dependency on the DOM.

React is more than just a DOM app library, it is a component
abstraction library. This enforces that.
This commit is contained in:
Jordan Walke
2013-08-15 10:56:39 -07:00
committed by Paul O’Shannessy
parent e6b216bdbb
commit 61c47e4cae
5 changed files with 201 additions and 108 deletions
+33 -104
View File
@@ -16,17 +16,11 @@
* @providesModule ReactComponent
*/
/*jslint evil: true */
"use strict";
var getReactRootElementInContainer = require('getReactRootElementInContainer');
var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactDOMIDOperations = require('ReactDOMIDOperations');
var ReactMarkupChecksum = require('ReactMarkupChecksum');
var ReactMount = require('ReactMount');
var ReactOwner = require('ReactOwner');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var ReactUpdates = require('ReactUpdates');
var invariant = require('invariant');
@@ -195,12 +189,33 @@ var ReactComponent = {
LifeCycle: ComponentLifeCycle,
/**
* React references `ReactDOMIDOperations` using this property in order to
* allow dependency injection.
* Injected module that provides ability to mutate individual properties.
* Injected into the base class because many different subclasses need access
* to this.
*
* @internal
*/
DOMIDOperations: ReactDOMIDOperations,
DOMIDOperations: ReactComponentEnvironment.DOMIDOperations,
/**
* 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
*/
unmountIDFromEnvironment: ReactComponentEnvironment.unmountIDFromEnvironment,
/**
* 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
*/
mountImageIntoNode: ReactComponentEnvironment.mountImageIntoNode,
/**
* React references `ReactReconcileTransaction` using this property in order
@@ -208,30 +223,16 @@ var ReactComponent = {
*
* @internal
*/
ReactReconcileTransaction: ReactReconcileTransaction,
ReactReconcileTransaction:
ReactComponentEnvironment.ReactReconcileTransaction,
/**
* @param {object} DOMIDOperations
* @final
*/
setDOMOperations: function(DOMIDOperations) {
ReactComponent.DOMIDOperations = DOMIDOperations;
},
/**
* @param {Transaction} ReactReconcileTransaction
* @final
*/
setReactReconcileTransaction: function(ReactReconcileTransaction) {
ReactComponent.ReactReconcileTransaction = ReactReconcileTransaction;
},
/**
* Base functionality for every ReactComponent constructor.
* Base functionality for every ReactComponent constructor. Mixed into the
* `ReactComponent` prototype, but exposed statically for easy access.
*
* @lends {ReactComponent.prototype}
*/
Mixin: {
Mixin: merge(ReactComponentEnvironment.Mixin, {
/**
* Checks whether or not this component is mounted.
@@ -244,21 +245,6 @@ var ReactComponent = {
return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
},
/**
* Returns the DOM node rendered by this component.
*
* @return {DOMElement} The root node of this component.
* @final
* @protected
*/
getDOMNode: function() {
invariant(
this.isMounted(),
'getDOMNode(): A component must be mounted to have a DOM node.'
);
return ReactMount.getNode(this._rootNodeID);
},
/**
* Sets a subset of the props.
*
@@ -382,7 +368,7 @@ var ReactComponent = {
if (props.ref != null) {
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
}
ReactMount.purgeID(this._rootNodeID);
ReactComponent.unmountIDFromEnvironment(this._rootNodeID);
this._rootNodeID = null;
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
},
@@ -495,63 +481,8 @@ var ReactComponent = {
container,
transaction,
shouldReuseMarkup) {
invariant(
container && container.nodeType === 1,
'mountComponentIntoNode(...): Target container is not a DOM element.'
);
var markup = this.mountComponent(rootID, transaction);
if (shouldReuseMarkup) {
if (ReactMarkupChecksum.canReuseMarkup(
markup,
getReactRootElementInContainer(container))) {
return;
} else {
if (__DEV__) {
console.warn(
'React attempted to use reuse markup in a container but the ' +
'checksum was invalid. This generally means that you are using ' +
'server rendering and the markup generated on the server was ' +
'not what the client was expecting. React injected new markup ' +
'to compensate which works but you have lost many of the ' +
'benefits of server rendering. Instead, figure out why the ' +
'markup being generated is different on the client or server.'
);
}
}
}
// Asynchronously inject markup by ensuring that the container is not in
// the document when settings its `innerHTML`.
var parent = container.parentNode;
if (parent) {
var next = container.nextSibling;
parent.removeChild(container);
container.innerHTML = markup;
if (next) {
parent.insertBefore(container, next);
} else {
parent.appendChild(container);
}
} else {
container.innerHTML = markup;
}
},
/**
* Unmounts this component and removes it from the DOM.
*
* @param {DOMElement} container DOM element to unmount from.
* @final
* @internal
* @see {ReactMount.unmountAndReleaseReactRootNode}
*/
unmountComponentFromNode: function(container) {
this.unmountComponent();
// http://jsperf.com/emptying-a-node
while (container.lastChild) {
container.removeChild(container.lastChild);
}
ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup);
},
/**
@@ -581,9 +512,7 @@ var ReactComponent = {
}
return owner.refs[ref];
}
}
})
};
module.exports = ReactComponent;
@@ -0,0 +1,122 @@
/**
* 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 ReactComponentBrowserEnvironment
*/
/*jslint evil: true */
"use strict";
var ReactDOMIDOperations = require('ReactDOMIDOperations');
var ReactMarkupChecksum = require('ReactMarkupChecksum');
var ReactMount = require('ReactMount');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var getReactRootElementInContainer = require('getReactRootElementInContainer');
var invariant = require('invariant');
/**
* Abstracts away all functionality of `ReactComponent` requires knowledge of
* the browser context.
*/
var ReactComponentBrowserEnvironment = {
/**
* Mixed into every component instance.
*/
Mixin: {
/**
* Returns the DOM node rendered by this component.
*
* @return {DOMElement} The root node of this component.
* @final
* @protected
*/
getDOMNode: function() {
invariant(
this.isMounted(),
'getDOMNode(): A component must be mounted to have a DOM node.'
);
return ReactMount.getNode(this._rootNodeID);
}
},
ReactReconcileTransaction: ReactReconcileTransaction,
DOMIDOperations: ReactDOMIDOperations,
/**
* If a particular environment requires that some resources be cleaned up,
* specify this in the injected Mixin. In the DOM, we would likely want to
* purge any cached node ID lookups.
*
* @private
*/
unmountIDFromEnvironment: function(rootNodeID) {
ReactMount.purgeID(rootNodeID);
},
/**
* @param {string} markup Markup string to place into the DOM Element.
* @param {DOMElement} container DOM Element to insert markup into.
* @param {boolean} shouldReuseMarkup Should reuse the existing markup in the
* container if possible.
*/
mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
invariant(
container && container.nodeType === 1,
'mountComponentIntoNode(...): Target container is not a DOM element.'
);
if (shouldReuseMarkup) {
if (ReactMarkupChecksum.canReuseMarkup(
markup,
getReactRootElementInContainer(container))) {
return;
} else {
if (__DEV__) {
console.warn(
'React attempted to use reuse markup in a container but the ' +
'checksum was invalid. This generally means that you are using ' +
'server rendering and the markup generated on the server was ' +
'not what the client was expecting. React injected new markup ' +
'to compensate which works but you have lost many of the ' +
'benefits of server rendering. Instead, figure out why the ' +
'markup being generated is different on the client or server.'
);
}
}
}
// Asynchronously inject markup by ensuring that the container is not in
// the document when settings its `innerHTML`.
var parent = container.parentNode;
if (parent) {
var next = container.nextSibling;
parent.removeChild(container);
container.innerHTML = markup;
if (next) {
parent.insertBefore(container, next);
} else {
parent.appendChild(container);
}
} else {
container.innerHTML = markup;
}
}
};
module.exports = ReactComponentBrowserEnvironment;
+24
View File
@@ -0,0 +1,24 @@
/**
* 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 ReactComponentEnvironment
*/
var ReactComponentBrowserEnvironment =
require('ReactComponentBrowserEnvironment');
var ReactComponentEnvironment = ReactComponentBrowserEnvironment;
module.exports = ReactComponentEnvironment;
+1 -1
View File
@@ -44,7 +44,7 @@ var ReactEventEmitterMixin = {
* there's a touch top-level listeners, anchors don't receive clicks for some
* reason, and only in some cases).
*
* @param {*} Configuration passed through to `listenAtTopLevel`.
* @param {*} config Configuration passed through to `listenAtTopLevel`.
*/
ensureListening: function(config) {
if (!this._isListening) {
+21 -3
View File
@@ -225,7 +225,7 @@ var ReactMount = {
*
* @private
*/
prepareTopLevelEvents: function() {
prepareEnvironmentForDOM: function() {
ReactEventEmitter.ensureListening(ReactMount.useTouchEvents);
},
@@ -262,7 +262,7 @@ var ReactMount = {
* @return {string} reactRoot ID prefix
*/
_registerComponent: function(nextComponent, container) {
ReactMount.prepareTopLevelEvents();
ReactMount.prepareEnvironmentForDOM();
var reactRootID = ReactMount.registerContainer(container);
instanceByReactRootID[reactRootID] = nextComponent;
@@ -400,7 +400,7 @@ var ReactMount = {
if (!component) {
return false;
}
component.unmountComponentFromNode(container);
ReactMount.unmountComponentFromNode(component, container);
delete instanceByReactRootID[reactRootID];
delete containersByReactRootID[reactRootID];
if (__DEV__) {
@@ -409,6 +409,24 @@ var ReactMount = {
return true;
},
/**
* Unmounts a component and removes it from the DOM.
*
* @param {ReactComponent} instance React component instance.
* @param {DOMElement} container DOM element to unmount from.
* @final
* @internal
* @see {ReactMount.unmountAndReleaseReactRootNode}
*/
unmountComponentFromNode: function(instance, container) {
instance.unmountComponent();
// http://jsperf.com/emptying-a-node
while (container.lastChild) {
container.removeChild(container.lastChild);
}
},
/**
* Finds the container DOM element that contains React component to which the
* supplied DOM `id` belongs.