@samitouri / QOS-React-2 / commits / 61c47e4cae

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.

Jordan Walke committed Aug 15, 2013 at 10:42 UTC 61c47e4cae857fcc4b52d89eebe551ca4880e2d7
5 files changed +198 -105
src/core/ReactComponent.js
+30 -101
@@ -16,17 +16,11 @@
16 * @providesModule ReactComponent
17 */
18
19 -/*jslint evil: true */
20 -
19 "use strict";
20
23 -var getReactRootElementInContainer = require('getReactRootElementInContainer');
21 +var ReactComponentEnvironment = require('ReactComponentEnvironment');
22 var ReactCurrentOwner = require('ReactCurrentOwner');
25 -var ReactDOMIDOperations = require('ReactDOMIDOperations');
26 -var ReactMarkupChecksum = require('ReactMarkupChecksum');
27 -var ReactMount = require('ReactMount');
23 var ReactOwner = require('ReactOwner');
29 -var ReactReconcileTransaction = require('ReactReconcileTransaction');
24 var ReactUpdates = require('ReactUpdates');
25
26 var invariant = require('invariant');
@@ -195,43 +189,50 @@ var ReactComponent = {
189 LifeCycle: ComponentLifeCycle,
190
191 /**
198 - * React references `ReactDOMIDOperations` using this property in order to
199 - * allow dependency injection.
192 + * Injected module that provides ability to mutate individual properties.
193 + * Injected into the base class because many different subclasses need access
194 + * to this.
195 *
196 * @internal
197 */
203 - DOMIDOperations: ReactDOMIDOperations,
198 + DOMIDOperations: ReactComponentEnvironment.DOMIDOperations,
199
200 /**
206 - * React references `ReactReconcileTransaction` using this property in order
207 - * to allow dependency injection.
201 + * Optionally injectable environment dependent cleanup hook. (server vs.
202 + * browser etc). Example: A browser system caches DOM nodes based on component
203 + * ID and must remove that cache entry when this instance is unmounted.
204 *
209 - * @internal
205 + * @private
206 */
211 - ReactReconcileTransaction: ReactReconcileTransaction,
207 + unmountIDFromEnvironment: ReactComponentEnvironment.unmountIDFromEnvironment,
208
209 /**
214 - * @param {object} DOMIDOperations
215 - * @final
210 + * The "image" of a component tree, is the platform specific (typically
211 + * serialized) data that represents a tree of lower level UI building blocks.
212 + * On the web, this "image" is HTML markup which describes a construction of
213 + * low level `div` and `span` nodes. Other platforms may have different
214 + * encoding of this "image". This must be injected.
215 + *
216 + * @private
217 */
217 - setDOMOperations: function(DOMIDOperations) {
218 - ReactComponent.DOMIDOperations = DOMIDOperations;
219 - },
218 + mountImageIntoNode: ReactComponentEnvironment.mountImageIntoNode,
219
220 /**
222 - * @param {Transaction} ReactReconcileTransaction
223 - * @final
221 + * React references `ReactReconcileTransaction` using this property in order
222 + * to allow dependency injection.
223 + *
224 + * @internal
225 */
225 - setReactReconcileTransaction: function(ReactReconcileTransaction) {
226 - ReactComponent.ReactReconcileTransaction = ReactReconcileTransaction;
227 - },
226 + ReactReconcileTransaction:
227 + ReactComponentEnvironment.ReactReconcileTransaction,
228
229 /**
230 - * Base functionality for every ReactComponent constructor.
230 + * Base functionality for every ReactComponent constructor. Mixed into the
231 + * `ReactComponent` prototype, but exposed statically for easy access.
232 *
233 * @lends {ReactComponent.prototype}
234 */
234 - Mixin: {
235 + Mixin: merge(ReactComponentEnvironment.Mixin, {
236
237 /**
238 * Checks whether or not this component is mounted.
@@ -244,21 +245,6 @@ var ReactComponent = {
245 return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
246 },
247
247 - /**
248 - * Returns the DOM node rendered by this component.
249 - *
250 - * @return {DOMElement} The root node of this component.
251 - * @final
252 - * @protected
253 - */
254 - getDOMNode: function() {
255 - invariant(
256 - this.isMounted(),
257 - 'getDOMNode(): A component must be mounted to have a DOM node.'
258 - );
259 - return ReactMount.getNode(this._rootNodeID);
260 - },
261 -
248 /**
249 * Sets a subset of the props.
250 *
@@ -382,7 +368,7 @@ var ReactComponent = {
368 if (props.ref != null) {
369 ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
370 }
385 - ReactMount.purgeID(this._rootNodeID);
371 + ReactComponent.unmountIDFromEnvironment(this._rootNodeID);
372 this._rootNodeID = null;
373 this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
374 },
@@ -495,63 +481,8 @@ var ReactComponent = {
481 container,
482 transaction,
483 shouldReuseMarkup) {
498 - invariant(
499 - container && container.nodeType === 1,
500 - 'mountComponentIntoNode(...): Target container is not a DOM element.'
501 - );
484 var markup = this.mountComponent(rootID, transaction);
503 -
504 - if (shouldReuseMarkup) {
505 - if (ReactMarkupChecksum.canReuseMarkup(
506 - markup,
507 - getReactRootElementInContainer(container))) {
508 - return;
509 - } else {
510 - if (__DEV__) {
511 - console.warn(
512 - 'React attempted to use reuse markup in a container but the ' +
513 - 'checksum was invalid. This generally means that you are using ' +
514 - 'server rendering and the markup generated on the server was ' +
515 - 'not what the client was expecting. React injected new markup ' +
516 - 'to compensate which works but you have lost many of the ' +
517 - 'benefits of server rendering. Instead, figure out why the ' +
518 - 'markup being generated is different on the client or server.'
519 - );
520 - }
521 - }
522 - }
523 -
524 - // Asynchronously inject markup by ensuring that the container is not in
525 - // the document when settings its `innerHTML`.
526 - var parent = container.parentNode;
527 - if (parent) {
528 - var next = container.nextSibling;
529 - parent.removeChild(container);
530 - container.innerHTML = markup;
531 - if (next) {
532 - parent.insertBefore(container, next);
533 - } else {
534 - parent.appendChild(container);
535 - }
536 - } else {
537 - container.innerHTML = markup;
538 - }
539 - },
540 -
541 - /**
542 - * Unmounts this component and removes it from the DOM.
543 - *
544 - * @param {DOMElement} container DOM element to unmount from.
545 - * @final
546 - * @internal
547 - * @see {ReactMount.unmountAndReleaseReactRootNode}
548 - */
549 - unmountComponentFromNode: function(container) {
550 - this.unmountComponent();
551 - // http://jsperf.com/emptying-a-node
552 - while (container.lastChild) {
553 - container.removeChild(container.lastChild);
554 - }
485 + ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup);
486 },
487
488 /**
@@ -581,9 +512,7 @@ var ReactComponent = {
512 }
513 return owner.refs[ref];
514 }
584 -
585 - }
586 -
515 + })
516 };
517
518 module.exports = ReactComponent;
src/core/ReactComponentBrowserEnvironment.js new
+122
@@ -0,0 +1,122 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactComponentBrowserEnvironment
17 + */
18 +
19 +/*jslint evil: true */
20 +
21 +"use strict";
22 +
23 +var ReactDOMIDOperations = require('ReactDOMIDOperations');
24 +var ReactMarkupChecksum = require('ReactMarkupChecksum');
25 +var ReactMount = require('ReactMount');
26 +var ReactReconcileTransaction = require('ReactReconcileTransaction');
27 +
28 +var getReactRootElementInContainer = require('getReactRootElementInContainer');
29 +var invariant = require('invariant');
30 +
31 +
32 +
33 +/**
34 + * Abstracts away all functionality of `ReactComponent` requires knowledge of
35 + * the browser context.
36 + */
37 +var ReactComponentBrowserEnvironment = {
38 + /**
39 + * Mixed into every component instance.
40 + */
41 + Mixin: {
42 + /**
43 + * Returns the DOM node rendered by this component.
44 + *
45 + * @return {DOMElement} The root node of this component.
46 + * @final
47 + * @protected
48 + */
49 + getDOMNode: function() {
50 + invariant(
51 + this.isMounted(),
52 + 'getDOMNode(): A component must be mounted to have a DOM node.'
53 + );
54 + return ReactMount.getNode(this._rootNodeID);
55 + }
56 + },
57 +
58 + ReactReconcileTransaction: ReactReconcileTransaction,
59 +
60 + DOMIDOperations: ReactDOMIDOperations,
61 +
62 + /**
63 + * If a particular environment requires that some resources be cleaned up,
64 + * specify this in the injected Mixin. In the DOM, we would likely want to
65 + * purge any cached node ID lookups.
66 + *
67 + * @private
68 + */
69 + unmountIDFromEnvironment: function(rootNodeID) {
70 + ReactMount.purgeID(rootNodeID);
71 + },
72 +
73 + /**
74 + * @param {string} markup Markup string to place into the DOM Element.
75 + * @param {DOMElement} container DOM Element to insert markup into.
76 + * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the
77 + * container if possible.
78 + */
79 + mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
80 + invariant(
81 + container && container.nodeType === 1,
82 + 'mountComponentIntoNode(...): Target container is not a DOM element.'
83 + );
84 + if (shouldReuseMarkup) {
85 + if (ReactMarkupChecksum.canReuseMarkup(
86 + markup,
87 + getReactRootElementInContainer(container))) {
88 + return;
89 + } else {
90 + if (__DEV__) {
91 + console.warn(
92 + 'React attempted to use reuse markup in a container but the ' +
93 + 'checksum was invalid. This generally means that you are using ' +
94 + 'server rendering and the markup generated on the server was ' +
95 + 'not what the client was expecting. React injected new markup ' +
96 + 'to compensate which works but you have lost many of the ' +
97 + 'benefits of server rendering. Instead, figure out why the ' +
98 + 'markup being generated is different on the client or server.'
99 + );
100 + }
101 + }
102 + }
103 +
104 + // Asynchronously inject markup by ensuring that the container is not in
105 + // the document when settings its `innerHTML`.
106 + var parent = container.parentNode;
107 + if (parent) {
108 + var next = container.nextSibling;
109 + parent.removeChild(container);
110 + container.innerHTML = markup;
111 + if (next) {
112 + parent.insertBefore(container, next);
113 + } else {
114 + parent.appendChild(container);
115 + }
116 + } else {
117 + container.innerHTML = markup;
118 + }
119 + }
120 +};
121 +
122 +module.exports = ReactComponentBrowserEnvironment;
src/core/ReactComponentEnvironment.js new
+24
@@ -0,0 +1,24 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactComponentEnvironment
17 + */
18 +
19 +var ReactComponentBrowserEnvironment =
20 + require('ReactComponentBrowserEnvironment');
21 +
22 +var ReactComponentEnvironment = ReactComponentBrowserEnvironment;
23 +
24 +module.exports = ReactComponentEnvironment;
src/core/ReactEventEmitterMixin.js
+1 -1
@@ -44,7 +44,7 @@ var ReactEventEmitterMixin = {
44 * there's a touch top-level listeners, anchors don't receive clicks for some
45 * reason, and only in some cases).
46 *
47 - * @param {*} Configuration passed through to `listenAtTopLevel`.
47 + * @param {*} config Configuration passed through to `listenAtTopLevel`.
48 */
49 ensureListening: function(config) {
50 if (!this._isListening) {
src/core/ReactMount.js
+21 -3
@@ -225,7 +225,7 @@ var ReactMount = {
225 *
226 * @private
227 */
228 - prepareTopLevelEvents: function() {
228 + prepareEnvironmentForDOM: function() {
229 ReactEventEmitter.ensureListening(ReactMount.useTouchEvents);
230 },
231
@@ -262,7 +262,7 @@ var ReactMount = {
262 * @return {string} reactRoot ID prefix
263 */
264 _registerComponent: function(nextComponent, container) {
265 - ReactMount.prepareTopLevelEvents();
265 + ReactMount.prepareEnvironmentForDOM();
266
267 var reactRootID = ReactMount.registerContainer(container);
268 instanceByReactRootID[reactRootID] = nextComponent;
@@ -400,7 +400,7 @@ var ReactMount = {
400 if (!component) {
401 return false;
402 }
403 - component.unmountComponentFromNode(container);
403 + ReactMount.unmountComponentFromNode(component, container);
404 delete instanceByReactRootID[reactRootID];
405 delete containersByReactRootID[reactRootID];
406 if (__DEV__) {
@@ -409,6 +409,24 @@ var ReactMount = {
409 return true;
410 },
411
412 + /**
413 + * Unmounts a component and removes it from the DOM.
414 + *
415 + * @param {ReactComponent} instance React component instance.
416 + * @param {DOMElement} container DOM element to unmount from.
417 + * @final
418 + * @internal
419 + * @see {ReactMount.unmountAndReleaseReactRootNode}
420 + */
421 + unmountComponentFromNode: function(instance, container) {
422 + instance.unmountComponent();
423 +
424 + // http://jsperf.com/emptying-a-node
425 + while (container.lastChild) {
426 + container.removeChild(container.lastChild);
427 + }
428 + },
429 +
430 /**
431 * Finds the container DOM element that contains React component to which the
432 * supplied DOM `id` belongs.