@samitouri / QOS-React-2 / commits / bfadafe5d0

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 +});