Skip to content

Commit

Permalink
Shallow rendering support (facebook#2393)
Browse files Browse the repository at this point in the history
Now handles updating. Haven't looked at refs yet.
  • Loading branch information
Scott Feeney committed Nov 17, 2014
1 parent e4218cb commit bfadafe
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 12 deletions.
146 changes: 135 additions & 11 deletions src/core/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,25 @@ var ReactCompositeComponentMixin = assign({},
}
},

/**
* @private
*/
_renderValidatedComponentWithoutOwnerOrContext: function() {
var inst = this._instance;
var renderedComponent = inst.render();
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof renderedComponent === 'undefined' &&
inst.render._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
renderedComponent = null;
}
}

return renderedComponent;
},

/**
* @private
*/
Expand All @@ -665,16 +684,8 @@ var ReactCompositeComponentMixin = assign({},
ReactCurrentOwner.current = this;
var inst = this._instance;
try {
renderedComponent = inst.render();
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof renderedComponent === 'undefined' &&
inst.render._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
renderedComponent = null;
}
}
renderedComponent =
this._renderValidatedComponentWithoutOwnerOrContext();
} finally {
ReactContext.current = previousContext;
ReactCurrentOwner.current = null;
Expand Down Expand Up @@ -734,11 +745,124 @@ var ReactCompositeComponentMixin = assign({},

});

var ShallowMixin = assign({},
ReactCompositeComponentMixin, {

/**
* Initializes the component, renders markup, and registers event listeners.
*
* @param {string} rootID DOM ID of the root node.
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {number} mountDepth number of components in the owner hierarchy
* @return {ReactElement} Shallow rendering of the component.
* @final
* @internal
*/
mountComponent: function(rootID, transaction, mountDepth) {
ReactComponent.Mixin.mountComponent.call(
this,
rootID,
transaction,
mountDepth
);

var inst = this._instance;

// Store a reference from the instance back to the internal representation
ReactInstanceMap.set(inst, this);

this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;

// No context for shallow-mounted components.
inst.props = this._processProps(this._currentElement.props);

var initialState = inst.getInitialState ? inst.getInitialState() : null;
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof initialState === 'undefined' &&
inst.getInitialState._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
initialState = null;
}
}
invariant(
typeof initialState === 'object' && !Array.isArray(initialState),
'%s.getInitialState(): must return an object or null',
inst.constructor.displayName || 'ReactCompositeComponent'
);
inst.state = initialState;

this._pendingState = null;
this._pendingForceUpdate = false;

if (inst.componentWillMount) {
inst.componentWillMount();
// When mounting, calls to `setState` by `componentWillMount` will set
// `this._pendingState` without triggering a re-render.
if (this._pendingState) {
inst.state = this._pendingState;
this._pendingState = null;
}
}

// No recursive call to instantiateReactComponent for shallow rendering.
this._renderedComponent =
this._renderValidatedComponentWithoutOwnerOrContext();

// Done with mounting, `setState` will now trigger UI changes.
this._compositeLifeCycleState = null;

// No call to this._renderedComponent.mountComponent for shallow
// rendering.

if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}

return this._renderedComponent;
},

/**
* Call the component's `render` method and update the DOM accordingly.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_updateRenderedComponent: function(transaction) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
// Use the without-owner-or-context variant of _rVC below:
var nextRenderedElement = this._renderValidatedComponentWithoutOwnerOrContext();
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
prevComponentInstance.receiveComponent(
nextRenderedElement,
transaction
);
} else {
// These two IDs are actually the same! But nothing should rely on that.
var thisID = this._rootNodeID;
var prevComponentID = prevComponentInstance._rootNodeID;
// Don't unmount previous instance since it was never mounted, due to
// shallow render.
//prevComponentInstance.unmountComponent();
this._renderedComponent = nextRenderedElement;
// ^ no instantiateReactComponent
//
// no recursive mountComponent
return nextRenderedElement;
}
}

});

var ReactCompositeComponent = {

LifeCycle: CompositeLifeCycle,

Mixin: ReactCompositeComponentMixin
Mixin: ReactCompositeComponentMixin,

ShallowMixin: ShallowMixin

};

Expand Down
16 changes: 15 additions & 1 deletion src/core/ReactElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,21 @@ var ReactElement = function(type, key, ref, owner, context, props) {
// an external backing store so that we can freeze the whole object.
// This can be replaced with a WeakMap once they are implemented in
// commonly used development environments.
this._store = { validated: false, props: props };
this._store = { props: props };

// To make comparing ReactElements easier for testing purposes, we make
// the validation flag non-enumerable (where possible, which should
// include every environment we run tests in), so the test framework
// ignores it.
try {
Object.defineProperty(this._store, 'validated', {
configurable: false,
enumerable: false,
writable: true
});
} catch (x) {
}
this._store.validated = false;

// We're not allowed to set props directly on the object so we early
// return and rely on the prototype membrane to forward to the backing
Expand Down
46 changes: 46 additions & 0 deletions src/test/ReactTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ var EventPropagators = require('EventPropagators');
var React = require('React');
var ReactElement = require('ReactElement');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactMount = require('ReactMount');
var ReactUpdates = require('ReactUpdates');
var SyntheticEvent = require('SyntheticEvent');

var assign = require('Object.assign');
var instantiateReactComponent = require('instantiateReactComponent');

var topLevelTypes = EventConstants.topLevelTypes;

Expand Down Expand Up @@ -298,10 +301,53 @@ var ReactTestUtils = {
};
},

createRenderer: function() {
return new ReactShallowRenderer();
},

Simulate: null,
SimulateNative: {}
};

/**
* @class ReactShallowRenderer
*/
var ReactShallowRenderer = function() {
this._instance = null;
};

ReactShallowRenderer.prototype.getRenderOutput = function() {
return (this._instance && this._instance._renderedComponent) || null;
};

var ShallowComponentWrapper = function(inst) {
this._instance = inst;
}
assign(
ShallowComponentWrapper.prototype,
ReactCompositeComponent.ShallowMixin
);

ReactShallowRenderer.prototype.render = function(element) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
this._render(element, transaction);
ReactUpdates.ReactReconcileTransaction.release(transaction);
};

ReactShallowRenderer.prototype._render = function(element, transaction) {
if (!this._instance) {
var rootID = ReactInstanceHandles.createReactRootID();
var instance = new ShallowComponentWrapper(new element.type(element.props));
instance.construct(element);

instance.mountComponent(rootID, transaction, 0);

this._instance = instance;
} else {
this._instance.receiveComponent(element, transaction);
}
};

/**
* Exports:
*
Expand Down
113 changes: 113 additions & 0 deletions src/test/__tests__/ReactTestUtils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Copyright 2013-2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

"use strict";

var React;
var ReactTestUtils;

var mocks;
var warn;

describe('ReactTestUtils', function() {

beforeEach(function() {
mocks = require('mocks');

React = require('React');
ReactTestUtils = require('ReactTestUtils');

warn = console.warn;
console.warn = mocks.getMockFunction();
});

afterEach(function() {
console.warn = warn;
});

it('should have shallow rendering', function() {
var SomeComponent = React.createClass({
render: function() {
return (
<div>
<span className="child1" />
<span className="child2" />
</div>
);
}
});

var shallowRenderer = ReactTestUtils.createRenderer();
shallowRenderer.render(<SomeComponent />);

var result = shallowRenderer.getRenderOutput();

expect(result.type).toBe('div');
expect(result.props.children).toEqual([
<span className="child1" />,
<span className="child2" />
]);
});

it('lets you update shallowly rendered components', function() {
var SomeComponent = React.createClass({
getInitialState: function() {
return {clicked: false};
},

onClick: function() {
this.setState({clicked: true});
},

render: function() {
var className = this.state.clicked ? 'was-clicked' : '';

if (this.props.aNew === 'prop') {
return (
<a
href="#"
onClick={this.onClick}
className={className}>
Test link
</a>
);
} else {
return (
<div>
<span className="child1" />
<span className="child2" />
</div>
);
}
}
});

var shallowRenderer = ReactTestUtils.createRenderer();
shallowRenderer.render(<SomeComponent />);
var result = shallowRenderer.getRenderOutput();
expect(result.type).toBe('div');
expect(result.props.children).toEqual([
<span className="child1" />,
<span className="child2" />
]);

shallowRenderer.render(<SomeComponent aNew="prop" />);
var updatedResult = shallowRenderer.getRenderOutput();
expect(updatedResult.type).toBe('a');

var mockEvent = {};
updatedResult.props.onClick(mockEvent);

var updatedResultCausedByClick = shallowRenderer.getRenderOutput();
expect(updatedResultCausedByClick.type).toBe('a');
expect(updatedResultCausedByClick.props.className).toBe('was-clicked');
});
});

0 comments on commit bfadafe

Please sign in to comment.