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.