Shallow rendering support (#2393)
Now handles updating. Haven't looked at refs yet.
Scott Feeney committed
Nov 10, 2014 at 14:57 UTC
bfadafe5d0322ba47402093e13f479c01c46ddda
4 files changed
+309
-12
src/core/ReactCompositeComponent.js
+135
-11
@@ -650,6 +650,25 @@ var ReactCompositeComponentMixin = assign({},
650
}
651
},
652
653
+ /**
654
+ * @private
655
+ */
656
+ _renderValidatedComponentWithoutOwnerOrContext: function() {
657
+ var inst = this._instance;
658
+ var renderedComponent = inst.render();
659
+ if (__DEV__) {
660
+ // We allow auto-mocks to proceed as if they're returning null.
661
+ if (typeof renderedComponent === 'undefined' &&
662
+ inst.render._isMockFunction) {
663
+ // This is probably bad practice. Consider warning here and
664
+ // deprecating this convenience.
665
+ renderedComponent = null;
666
+ }
667
+ }
668
+
669
+ return renderedComponent;
670
+ },
671
+
672
/**
673
* @private
674
*/
@@ -665,16 +684,8 @@ var ReactCompositeComponentMixin = assign({},
684
ReactCurrentOwner.current = this;
685
var inst = this._instance;
686
try {
668
- renderedComponent = inst.render();
669
- if (__DEV__) {
670
- // We allow auto-mocks to proceed as if they're returning null.
671
- if (typeof renderedComponent === 'undefined' &&
672
- inst.render._isMockFunction) {
673
- // This is probably bad practice. Consider warning here and
674
- // deprecating this convenience.
675
- renderedComponent = null;
676
- }
677
- }
687
+ renderedComponent =
688
+ this._renderValidatedComponentWithoutOwnerOrContext();
689
} finally {
690
ReactContext.current = previousContext;
691
ReactCurrentOwner.current = null;
@@ -734,11 +745,124 @@ var ReactCompositeComponentMixin = assign({},
745
746
});
747
748
+var ShallowMixin = assign({},
749
+ ReactCompositeComponentMixin, {
750
+
751
+ /**
752
+ * Initializes the component, renders markup, and registers event listeners.
753
+ *
754
+ * @param {string} rootID DOM ID of the root node.
755
+ * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
756
+ * @param {number} mountDepth number of components in the owner hierarchy
757
+ * @return {ReactElement} Shallow rendering of the component.
758
+ * @final
759
+ * @internal
760
+ */
761
+ mountComponent: function(rootID, transaction, mountDepth) {
762
+ ReactComponent.Mixin.mountComponent.call(
763
+ this,
764
+ rootID,
765
+ transaction,
766
+ mountDepth
767
+ );
768
+
769
+ var inst = this._instance;
770
+
771
+ // Store a reference from the instance back to the internal representation
772
+ ReactInstanceMap.set(inst, this);
773
+
774
+ this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
775
+
776
+ // No context for shallow-mounted components.
777
+ inst.props = this._processProps(this._currentElement.props);
778
+
779
+ var initialState = inst.getInitialState ? inst.getInitialState() : null;
780
+ if (__DEV__) {
781
+ // We allow auto-mocks to proceed as if they're returning null.
782
+ if (typeof initialState === 'undefined' &&
783
+ inst.getInitialState._isMockFunction) {
784
+ // This is probably bad practice. Consider warning here and
785
+ // deprecating this convenience.
786
+ initialState = null;
787
+ }
788
+ }
789
+ invariant(
790
+ typeof initialState === 'object' && !Array.isArray(initialState),
791
+ '%s.getInitialState(): must return an object or null',
792
+ inst.constructor.displayName || 'ReactCompositeComponent'
793
+ );
794
+ inst.state = initialState;
795
+
796
+ this._pendingState = null;
797
+ this._pendingForceUpdate = false;
798
+
799
+ if (inst.componentWillMount) {
800
+ inst.componentWillMount();
801
+ // When mounting, calls to `setState` by `componentWillMount` will set
802
+ // `this._pendingState` without triggering a re-render.
803
+ if (this._pendingState) {
804
+ inst.state = this._pendingState;
805
+ this._pendingState = null;
806
+ }
807
+ }
808
+
809
+ // No recursive call to instantiateReactComponent for shallow rendering.
810
+ this._renderedComponent =
811
+ this._renderValidatedComponentWithoutOwnerOrContext();
812
+
813
+ // Done with mounting, `setState` will now trigger UI changes.
814
+ this._compositeLifeCycleState = null;
815
+
816
+ // No call to this._renderedComponent.mountComponent for shallow
817
+ // rendering.
818
+
819
+ if (inst.componentDidMount) {
820
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
821
+ }
822
+
823
+ return this._renderedComponent;
824
+ },
825
+
826
+ /**
827
+ * Call the component's `render` method and update the DOM accordingly.
828
+ *
829
+ * @param {ReactReconcileTransaction} transaction
830
+ * @internal
831
+ */
832
+ _updateRenderedComponent: function(transaction) {
833
+ var prevComponentInstance = this._renderedComponent;
834
+ var prevRenderedElement = prevComponentInstance._currentElement;
835
+ // Use the without-owner-or-context variant of _rVC below:
836
+ var nextRenderedElement = this._renderValidatedComponentWithoutOwnerOrContext();
837
+ if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
838
+ prevComponentInstance.receiveComponent(
839
+ nextRenderedElement,
840
+ transaction
841
+ );
842
+ } else {
843
+ // These two IDs are actually the same! But nothing should rely on that.
844
+ var thisID = this._rootNodeID;
845
+ var prevComponentID = prevComponentInstance._rootNodeID;
846
+ // Don't unmount previous instance since it was never mounted, due to
847
+ // shallow render.
848
+ //prevComponentInstance.unmountComponent();
849
+ this._renderedComponent = nextRenderedElement;
850
+ // ^ no instantiateReactComponent
851
+ //
852
+ // no recursive mountComponent
853
+ return nextRenderedElement;
854
+ }
855
+ }
856
+
857
+});
858
+
859
var ReactCompositeComponent = {
860
861
LifeCycle: CompositeLifeCycle,
862
741
- Mixin: ReactCompositeComponentMixin
863
+ Mixin: ReactCompositeComponentMixin,
864
+
865
+ ShallowMixin: ShallowMixin
866
867
};
868
src/core/ReactElement.js
+15
-1
@@ -106,7 +106,21 @@ var ReactElement = function(type, key, ref, owner, context, props) {
106
// an external backing store so that we can freeze the whole object.
107
// This can be replaced with a WeakMap once they are implemented in
108
// commonly used development environments.
109
- this._store = { validated: false, props: props };
109
+ this._store = { props: props };
110
+
111
+ // To make comparing ReactElements easier for testing purposes, we make
112
+ // the validation flag non-enumerable (where possible, which should
113
+ // include every environment we run tests in), so the test framework
114
+ // ignores it.
115
+ try {
116
+ Object.defineProperty(this._store, 'validated', {
117
+ configurable: false,
118
+ enumerable: false,
119
+ writable: true
120
+ });
121
+ } catch (x) {
122
+ }
123
+ this._store.validated = false;
124
125
// We're not allowed to set props directly on the object so we early
126
// return and rely on the prototype membrane to forward to the backing
src/test/ReactTestUtils.js
+46
@@ -17,12 +17,15 @@ var EventPropagators = require('EventPropagators');
17
var React = require('React');
18
var ReactElement = require('ReactElement');
19
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
20
+var ReactCompositeComponent = require('ReactCompositeComponent');
21
+var ReactInstanceHandles = require('ReactInstanceHandles');
22
var ReactInstanceMap = require('ReactInstanceMap');
23
var ReactMount = require('ReactMount');
24
var ReactUpdates = require('ReactUpdates');
25
var SyntheticEvent = require('SyntheticEvent');
26
27
var assign = require('Object.assign');
28
+var instantiateReactComponent = require('instantiateReactComponent');
29
30
var topLevelTypes = EventConstants.topLevelTypes;
31
@@ -298,10 +301,53 @@ var ReactTestUtils = {
301
};
302
},
303
304
+ createRenderer: function() {
305
+ return new ReactShallowRenderer();
306
+ },
307
+
308
Simulate: null,
309
SimulateNative: {}
310
};
311
312
+/**
313
+ * @class ReactShallowRenderer
314
+ */
315
+var ReactShallowRenderer = function() {
316
+ this._instance = null;
317
+};
318
+
319
+ReactShallowRenderer.prototype.getRenderOutput = function() {
320
+ return (this._instance && this._instance._renderedComponent) || null;
321
+};
322
+
323
+var ShallowComponentWrapper = function(inst) {
324
+ this._instance = inst;
325
+}
326
+assign(
327
+ ShallowComponentWrapper.prototype,
328
+ ReactCompositeComponent.ShallowMixin
329
+);
330
+
331
+ReactShallowRenderer.prototype.render = function(element) {
332
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
333
+ this._render(element, transaction);
334
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
335
+};
336
+
337
+ReactShallowRenderer.prototype._render = function(element, transaction) {
338
+ if (!this._instance) {
339
+ var rootID = ReactInstanceHandles.createReactRootID();
340
+ var instance = new ShallowComponentWrapper(new element.type(element.props));
341
+ instance.construct(element);
342
+
343
+ instance.mountComponent(rootID, transaction, 0);
344
+
345
+ this._instance = instance;
346
+ } else {
347
+ this._instance.receiveComponent(element, transaction);
348
+ }
349
+};
350
+
351
/**
352
* Exports:
353
*
src/test/__tests__/ReactTestUtils-test.js
new
+113
@@ -0,0 +1,113 @@
1
+/**
2
+ * Copyright 2013-2014, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+"use strict";
13
+
14
+var React;
15
+var ReactTestUtils;
16
+
17
+var mocks;
18
+var warn;
19
+
20
+describe('ReactTestUtils', function() {
21
+
22
+ beforeEach(function() {
23
+ mocks = require('mocks');
24
+
25
+ React = require('React');
26
+ ReactTestUtils = require('ReactTestUtils');
27
+
28
+ warn = console.warn;
29
+ console.warn = mocks.getMockFunction();
30
+ });
31
+
32
+ afterEach(function() {
33
+ console.warn = warn;
34
+ });
35
+
36
+ it('should have shallow rendering', function() {
37
+ var SomeComponent = React.createClass({
38
+ render: function() {
39
+ return (
40
+ <div>
41
+ <span className="child1" />
42
+ <span className="child2" />
43
+ </div>
44
+ );
45
+ }
46
+ });
47
+
48
+ var shallowRenderer = ReactTestUtils.createRenderer();
49
+ shallowRenderer.render(<SomeComponent />);
50
+
51
+ var result = shallowRenderer.getRenderOutput();
52
+
53
+ expect(result.type).toBe('div');
54
+ expect(result.props.children).toEqual([
55
+ <span className="child1" />,
56
+ <span className="child2" />
57
+ ]);
58
+ });
59
+
60
+ it('lets you update shallowly rendered components', function() {
61
+ var SomeComponent = React.createClass({
62
+ getInitialState: function() {
63
+ return {clicked: false};
64
+ },
65
+
66
+ onClick: function() {
67
+ this.setState({clicked: true});
68
+ },
69
+
70
+ render: function() {
71
+ var className = this.state.clicked ? 'was-clicked' : '';
72
+
73
+ if (this.props.aNew === 'prop') {
74
+ return (
75
+ <a
76
+ href="#"
77
+ onClick={this.onClick}
78
+ className={className}>
79
+ Test link
80
+ </a>
81
+ );
82
+ } else {
83
+ return (
84
+ <div>
85
+ <span className="child1" />
86
+ <span className="child2" />
87
+ </div>
88
+ );
89
+ }
90
+ }
91
+ });
92
+
93
+ var shallowRenderer = ReactTestUtils.createRenderer();
94
+ shallowRenderer.render(<SomeComponent />);
95
+ var result = shallowRenderer.getRenderOutput();
96
+ expect(result.type).toBe('div');
97
+ expect(result.props.children).toEqual([
98
+ <span className="child1" />,
99
+ <span className="child2" />
100
+ ]);
101
+
102
+ shallowRenderer.render(<SomeComponent aNew="prop" />);
103
+ var updatedResult = shallowRenderer.getRenderOutput();
104
+ expect(updatedResult.type).toBe('a');
105
+
106
+ var mockEvent = {};
107
+ updatedResult.props.onClick(mockEvent);
108
+
109
+ var updatedResultCausedByClick = shallowRenderer.getRenderOutput();
110
+ expect(updatedResultCausedByClick.type).toBe('a');
111
+ expect(updatedResultCausedByClick.props.className).toBe('was-clicked');
112
+ });
113
+});