Skip to content

Commit bfadafe

Browse files
author
Scott Feeney
committed
Shallow rendering support (facebook#2393)
Now handles updating. Haven't looked at refs yet.
1 parent e4218cb commit bfadafe

File tree

4 files changed

+309
-12
lines changed

4 files changed

+309
-12
lines changed

src/core/ReactCompositeComponent.js

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,25 @@ var ReactCompositeComponentMixin = assign({},
650650
}
651651
},
652652

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+
653672
/**
654673
* @private
655674
*/
@@ -665,16 +684,8 @@ var ReactCompositeComponentMixin = assign({},
665684
ReactCurrentOwner.current = this;
666685
var inst = this._instance;
667686
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();
678689
} finally {
679690
ReactContext.current = previousContext;
680691
ReactCurrentOwner.current = null;
@@ -734,11 +745,124 @@ var ReactCompositeComponentMixin = assign({},
734745

735746
});
736747

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+
737859
var ReactCompositeComponent = {
738860

739861
LifeCycle: CompositeLifeCycle,
740862

741-
Mixin: ReactCompositeComponentMixin
863+
Mixin: ReactCompositeComponentMixin,
864+
865+
ShallowMixin: ShallowMixin
742866

743867
};
744868

src/core/ReactElement.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,21 @@ var ReactElement = function(type, key, ref, owner, context, props) {
106106
// an external backing store so that we can freeze the whole object.
107107
// This can be replaced with a WeakMap once they are implemented in
108108
// 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;
110124

111125
// We're not allowed to set props directly on the object so we early
112126
// return and rely on the prototype membrane to forward to the backing

src/test/ReactTestUtils.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ var EventPropagators = require('EventPropagators');
1717
var React = require('React');
1818
var ReactElement = require('ReactElement');
1919
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
20+
var ReactCompositeComponent = require('ReactCompositeComponent');
21+
var ReactInstanceHandles = require('ReactInstanceHandles');
2022
var ReactInstanceMap = require('ReactInstanceMap');
2123
var ReactMount = require('ReactMount');
2224
var ReactUpdates = require('ReactUpdates');
2325
var SyntheticEvent = require('SyntheticEvent');
2426

2527
var assign = require('Object.assign');
28+
var instantiateReactComponent = require('instantiateReactComponent');
2629

2730
var topLevelTypes = EventConstants.topLevelTypes;
2831

@@ -298,10 +301,53 @@ var ReactTestUtils = {
298301
};
299302
},
300303

304+
createRenderer: function() {
305+
return new ReactShallowRenderer();
306+
},
307+
301308
Simulate: null,
302309
SimulateNative: {}
303310
};
304311

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+
305351
/**
306352
* Exports:
307353
*
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});

0 commit comments

Comments
 (0)