Minimal implementation of stateless components

Stateless pure-function components give us more opportunity to make performance optimizations. For now, we'll do a minimal implementation which has similar performance characteristics to other components in the interests of shipping 0.14 and allowing people to begin writing code using this pattern; in the future we can refactor to allocate less and avoid other unnecessary work.
This commit is contained in:
Ben Alpert
2015-08-07 19:27:42 -07:00
parent 3ec9f86573
commit 5a7bd964b4
3 changed files with 230 additions and 13 deletions
@@ -38,6 +38,13 @@ function getDeclarationErrorAddendum(component) {
return '';
}
function StatelessComponent(Component) {
}
StatelessComponent.prototype.render = function() {
var Component = ReactInstanceMap.get(this)._currentElement.type;
return new Component(this.props, this.context, this.updater);
};
/**
* ------------------ The Life-Cycle of a Composite Component ------------------
*
@@ -126,7 +133,24 @@ var ReactCompositeComponentMixin = {
var Component = this._currentElement.type;
// Initialize the public class
var inst = new Component(publicProps, publicContext, ReactUpdateQueue);
var inst;
var renderedElement;
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
} finally {
ReactCurrentOwner.current = null;
}
} else {
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
}
if (inst === null || inst === false || ReactElement.isValidElement(inst)) {
renderedElement = inst;
inst = new StatelessComponent(Component);
}
if (__DEV__) {
// This will throw later in _renderValidatedComponent, but add an early
@@ -231,7 +255,10 @@ var ReactCompositeComponentMixin = {
}
}
var renderedElement = this._renderValidatedComponent();
// If not a stateless component, we now render
if (renderedElement === undefined) {
renderedElement = this._renderValidatedComponent();
}
this._renderedComponent = this._instantiateReactComponent(
renderedElement
@@ -265,6 +292,7 @@ var ReactCompositeComponentMixin = {
ReactReconciler.unmountComponent(this._renderedComponent);
this._renderedComponent = null;
this._instance = null;
// Reset pending fields
// Even if this component is scheduled for another update in ReactUpdates,
@@ -759,6 +787,7 @@ var ReactCompositeComponentMixin = {
*/
attachRef: function(ref, component) {
var inst = this.getPublicInstance();
invariant(inst != null, 'Stateless function components cannot have refs.');
var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
refs[ref] = component.getPublicInstance();
},
@@ -800,7 +829,11 @@ var ReactCompositeComponentMixin = {
* @internal
*/
getPublicInstance: function() {
return this._instance;
var inst = this._instance;
if (inst instanceof StatelessComponent) {
return null;
}
return inst;
},
// Stub
@@ -25,16 +25,6 @@ function enqueueUpdate(internalInstance) {
}
function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
if (__DEV__) {
warning(
ReactCurrentOwner.current == null,
'%s(...): Cannot update during an existing state transition ' +
'(such as within `render`). Render methods should be a pure function ' +
'of props and state.',
callerName
);
}
var internalInstance = ReactInstanceMap.get(publicInstance);
if (!internalInstance) {
if (__DEV__) {
@@ -54,6 +44,16 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
return null;
}
if (__DEV__) {
warning(
ReactCurrentOwner.current == null,
'%s(...): Cannot update during an existing state transition ' +
'(such as within `render`). Render methods should be a pure function ' +
'of props and state.',
callerName
);
}
return internalInstance;
}
@@ -0,0 +1,184 @@
/**
* Copyright 2013-2015, 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;
function StatelessComponent(props) {
return <div>{props.name}</div>;
}
describe('ReactStatelessComponent', function() {
beforeEach(function() {
React = require('React');
ReactTestUtils = require('ReactTestUtils');
});
it('should render stateless component', function() {
var el = document.createElement('div');
React.render(<StatelessComponent name="A" />, el);
expect(el.textContent).toBe('A');
});
it('should update stateless component', function() {
var Parent = React.createClass({
render() {
return <StatelessComponent {...this.props} />;
},
});
var el = document.createElement('div');
React.render(<Parent name="A" />, el);
expect(el.textContent).toBe('A');
React.render(<Parent name="B" />, el);
expect(el.textContent).toBe('B');
});
it('should unmount stateless component', function() {
var container = document.createElement('div');
React.render(<StatelessComponent name="A" />, container);
expect(container.textContent).toBe('A');
React.unmountComponentAtNode(container);
expect(container.textContent).toBe('');
});
it('should pass context thru stateless component', function() {
var Child = React.createClass({
contextTypes: {
test: React.PropTypes.string.isRequired,
},
render: function() {
return <div>{this.context.test}</div>;
},
});
function Parent() {
return <Child />;
}
var GrandParent = React.createClass({
childContextTypes: {
test: React.PropTypes.string.isRequired,
},
getChildContext() {
return {test: this.props.test};
},
render: function() {
return <Parent />;
},
});
var el = document.createElement('div');
React.render(<GrandParent test="test" />, el);
expect(el.textContent).toBe('test');
React.render(<GrandParent test="mest" />, el);
expect(el.textContent).toBe('mest');
});
it('should support module pattern components', function() {
function Child({test}) {
return {
render() {
return <div>{test}</div>;
},
};
}
var el = document.createElement('div');
React.render(<Child test="test" />, el);
expect(el.textContent).toBe('test');
});
it('should throw on string refs in pure functions', function() {
function Child() {
return <div ref="me" />;
}
expect(function() {
ReactTestUtils.renderIntoDocument(<Child test="test" />);
}).toThrow(
'Invariant Violation: Stateless function components cannot have refs.'
);
});
it('should provide a null ref', function() {
function Child() {
return <div />;
}
var comp = ReactTestUtils.renderIntoDocument(<Child />);
expect(comp).toBe(null);
});
it('should use correct name in key warning', function() {
function Child() {
return <div>{[<span />]}</div>;
}
spyOn(console, 'error');
ReactTestUtils.renderIntoDocument(<Child />);
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toContain('a unique "key" prop');
expect(console.error.argsForCall[0][0]).toContain('Child');
});
it('should support default props and prop types', function() {
function Child(props) {
return <div>{props.test}</div>;
}
Child.defaultProps = {test: 2};
Child.propTypes = {test: React.PropTypes.string};
spyOn(console, 'error');
ReactTestUtils.renderIntoDocument(<Child />);
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: Invalid prop `test` of type `number` ' +
'supplied to `Child`, expected `string`.'
);
});
it('should receive context', function() {
var Parent = React.createClass({
childContextTypes: {
lang: React.PropTypes.string,
},
getChildContext: function() {
return {lang: 'en'};
},
render: function() {
return <Child />;
},
});
function Child(props, context) {
return <div>{context.lang}</div>;
}
Child.contextTypes = {lang: React.PropTypes.string};
var el = document.createElement('div');
React.render(<Parent />, el);
expect(el.textContent).toBe('en');
});
});