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.
Ben Alpert committed
Aug 7, 2015 at 19:06 UTC
5a7bd964b4265a0e0e87e453fa0d1a4ccb604fe1
3 files changed
+230
-13
src/renderers/shared/reconciler/ReactCompositeComponent.js
+36
-3
@@ -38,6 +38,13 @@ function getDeclarationErrorAddendum(component) {
38
return '';
39
}
40
41
+function StatelessComponent(Component) {
42
+}
43
+StatelessComponent.prototype.render = function() {
44
+ var Component = ReactInstanceMap.get(this)._currentElement.type;
45
+ return new Component(this.props, this.context, this.updater);
46
+};
47
+
48
/**
49
* ------------------ The Life-Cycle of a Composite Component ------------------
50
*
@@ -126,7 +133,24 @@ var ReactCompositeComponentMixin = {
133
var Component = this._currentElement.type;
134
135
// Initialize the public class
129
- var inst = new Component(publicProps, publicContext, ReactUpdateQueue);
136
+ var inst;
137
+ var renderedElement;
138
+
139
+ if (__DEV__) {
140
+ ReactCurrentOwner.current = this;
141
+ try {
142
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
143
+ } finally {
144
+ ReactCurrentOwner.current = null;
145
+ }
146
+ } else {
147
+ inst = new Component(publicProps, publicContext, ReactUpdateQueue);
148
+ }
149
+
150
+ if (inst === null || inst === false || ReactElement.isValidElement(inst)) {
151
+ renderedElement = inst;
152
+ inst = new StatelessComponent(Component);
153
+ }
154
155
if (__DEV__) {
156
// This will throw later in _renderValidatedComponent, but add an early
@@ -231,7 +255,10 @@ var ReactCompositeComponentMixin = {
255
}
256
}
257
234
- var renderedElement = this._renderValidatedComponent();
258
+ // If not a stateless component, we now render
259
+ if (renderedElement === undefined) {
260
+ renderedElement = this._renderValidatedComponent();
261
+ }
262
263
this._renderedComponent = this._instantiateReactComponent(
264
renderedElement
@@ -265,6 +292,7 @@ var ReactCompositeComponentMixin = {
292
293
ReactReconciler.unmountComponent(this._renderedComponent);
294
this._renderedComponent = null;
295
+ this._instance = null;
296
297
// Reset pending fields
298
// Even if this component is scheduled for another update in ReactUpdates,
@@ -759,6 +787,7 @@ var ReactCompositeComponentMixin = {
787
*/
788
attachRef: function(ref, component) {
789
var inst = this.getPublicInstance();
790
+ invariant(inst != null, 'Stateless function components cannot have refs.');
791
var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
792
refs[ref] = component.getPublicInstance();
793
},
@@ -800,7 +829,11 @@ var ReactCompositeComponentMixin = {
829
* @internal
830
*/
831
getPublicInstance: function() {
803
- return this._instance;
832
+ var inst = this._instance;
833
+ if (inst instanceof StatelessComponent) {
834
+ return null;
835
+ }
836
+ return inst;
837
},
838
839
// Stub
src/renderers/shared/reconciler/ReactUpdateQueue.js
+10
-10
@@ -25,16 +25,6 @@ function enqueueUpdate(internalInstance) {
25
}
26
27
function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
28
- if (__DEV__) {
29
- warning(
30
- ReactCurrentOwner.current == null,
31
- '%s(...): Cannot update during an existing state transition ' +
32
- '(such as within `render`). Render methods should be a pure function ' +
33
- 'of props and state.',
34
- callerName
35
- );
36
- }
37
-
28
var internalInstance = ReactInstanceMap.get(publicInstance);
29
if (!internalInstance) {
30
if (__DEV__) {
@@ -54,6 +44,16 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
44
return null;
45
}
46
47
+ if (__DEV__) {
48
+ warning(
49
+ ReactCurrentOwner.current == null,
50
+ '%s(...): Cannot update during an existing state transition ' +
51
+ '(such as within `render`). Render methods should be a pure function ' +
52
+ 'of props and state.',
53
+ callerName
54
+ );
55
+ }
56
+
57
return internalInstance;
58
}
59
src/renderers/shared/reconciler/__tests__/ReactStatelessComponent-test.js
new
+184
@@ -0,0 +1,184 @@
1
+/**
2
+ * Copyright 2013-2015, 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
+function StatelessComponent(props) {
18
+ return <div>{props.name}</div>;
19
+}
20
+
21
+describe('ReactStatelessComponent', function() {
22
+
23
+ beforeEach(function() {
24
+ React = require('React');
25
+ ReactTestUtils = require('ReactTestUtils');
26
+ });
27
+
28
+ it('should render stateless component', function() {
29
+ var el = document.createElement('div');
30
+ React.render(<StatelessComponent name="A" />, el);
31
+
32
+ expect(el.textContent).toBe('A');
33
+ });
34
+
35
+ it('should update stateless component', function() {
36
+ var Parent = React.createClass({
37
+ render() {
38
+ return <StatelessComponent {...this.props} />;
39
+ },
40
+ });
41
+
42
+ var el = document.createElement('div');
43
+ React.render(<Parent name="A" />, el);
44
+ expect(el.textContent).toBe('A');
45
+
46
+ React.render(<Parent name="B" />, el);
47
+ expect(el.textContent).toBe('B');
48
+ });
49
+
50
+ it('should unmount stateless component', function() {
51
+ var container = document.createElement('div');
52
+
53
+ React.render(<StatelessComponent name="A" />, container);
54
+ expect(container.textContent).toBe('A');
55
+
56
+ React.unmountComponentAtNode(container);
57
+ expect(container.textContent).toBe('');
58
+ });
59
+
60
+ it('should pass context thru stateless component', function() {
61
+ var Child = React.createClass({
62
+ contextTypes: {
63
+ test: React.PropTypes.string.isRequired,
64
+ },
65
+
66
+ render: function() {
67
+ return <div>{this.context.test}</div>;
68
+ },
69
+ });
70
+
71
+ function Parent() {
72
+ return <Child />;
73
+ }
74
+
75
+ var GrandParent = React.createClass({
76
+ childContextTypes: {
77
+ test: React.PropTypes.string.isRequired,
78
+ },
79
+
80
+ getChildContext() {
81
+ return {test: this.props.test};
82
+ },
83
+
84
+ render: function() {
85
+ return <Parent />;
86
+ },
87
+ });
88
+
89
+ var el = document.createElement('div');
90
+ React.render(<GrandParent test="test" />, el);
91
+
92
+ expect(el.textContent).toBe('test');
93
+
94
+ React.render(<GrandParent test="mest" />, el);
95
+
96
+ expect(el.textContent).toBe('mest');
97
+ });
98
+
99
+ it('should support module pattern components', function() {
100
+ function Child({test}) {
101
+ return {
102
+ render() {
103
+ return <div>{test}</div>;
104
+ },
105
+ };
106
+ }
107
+
108
+ var el = document.createElement('div');
109
+ React.render(<Child test="test" />, el);
110
+
111
+ expect(el.textContent).toBe('test');
112
+ });
113
+
114
+ it('should throw on string refs in pure functions', function() {
115
+ function Child() {
116
+ return <div ref="me" />;
117
+ }
118
+
119
+ expect(function() {
120
+ ReactTestUtils.renderIntoDocument(<Child test="test" />);
121
+ }).toThrow(
122
+ 'Invariant Violation: Stateless function components cannot have refs.'
123
+ );
124
+ });
125
+
126
+ it('should provide a null ref', function() {
127
+ function Child() {
128
+ return <div />;
129
+ }
130
+
131
+ var comp = ReactTestUtils.renderIntoDocument(<Child />);
132
+ expect(comp).toBe(null);
133
+ });
134
+
135
+ it('should use correct name in key warning', function() {
136
+ function Child() {
137
+ return <div>{[<span />]}</div>;
138
+ }
139
+
140
+ spyOn(console, 'error');
141
+ ReactTestUtils.renderIntoDocument(<Child />);
142
+ expect(console.error.argsForCall.length).toBe(1);
143
+ expect(console.error.argsForCall[0][0]).toContain('a unique "key" prop');
144
+ expect(console.error.argsForCall[0][0]).toContain('Child');
145
+ });
146
+
147
+ it('should support default props and prop types', function() {
148
+ function Child(props) {
149
+ return <div>{props.test}</div>;
150
+ }
151
+ Child.defaultProps = {test: 2};
152
+ Child.propTypes = {test: React.PropTypes.string};
153
+
154
+ spyOn(console, 'error');
155
+ ReactTestUtils.renderIntoDocument(<Child />);
156
+ expect(console.error.argsForCall.length).toBe(1);
157
+ expect(console.error.argsForCall[0][0]).toBe(
158
+ 'Warning: Failed propType: Invalid prop `test` of type `number` ' +
159
+ 'supplied to `Child`, expected `string`.'
160
+ );
161
+ });
162
+
163
+ it('should receive context', function() {
164
+ var Parent = React.createClass({
165
+ childContextTypes: {
166
+ lang: React.PropTypes.string,
167
+ },
168
+ getChildContext: function() {
169
+ return {lang: 'en'};
170
+ },
171
+ render: function() {
172
+ return <Child />;
173
+ },
174
+ });
175
+ function Child(props, context) {
176
+ return <div>{context.lang}</div>;
177
+ }
178
+ Child.contextTypes = {lang: React.PropTypes.string};
179
+
180
+ var el = document.createElement('div');
181
+ React.render(<Parent />, el);
182
+ expect(el.textContent).toBe('en');
183
+ });
184
+});