Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic shallow rendering support #2497

Merged
merged 1 commit into from
Nov 17, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});