Make rendering to null work and make it injectable
This allows render to return `null`. Under the hood, this renders a `script` tag. closes #1127
Cheng Lou committed
Apr 8, 2014 at 10:10 UTC
f0f59877aa4de337d4c291ba9555f78632915446
6 files changed
+271
-4
src/browser/ui/ReactBrowserComponentMixin.js
+4
@@ -18,6 +18,7 @@
18
19
"use strict";
20
21
+var ReactEmptyComponent = require('ReactEmptyComponent');
22
var ReactMount = require('ReactMount');
23
24
var invariant = require('invariant');
@@ -35,6 +36,9 @@ var ReactBrowserComponentMixin = {
36
this.isMounted(),
37
'getDOMNode(): A component must be mounted to have a DOM node.'
38
);
39
+ if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) {
40
+ return null;
41
+ }
42
return ReactMount.getNode(this._rootNodeID);
43
}
44
};
src/browser/ui/ReactDefaultInjection.js
+2
@@ -101,6 +101,8 @@ function inject() {
101
102
ReactInjection.DOMProperty.injectDOMPropertyConfig(DefaultDOMPropertyConfig);
103
104
+ ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.script);
105
+
106
ReactInjection.Updates.injectBatchingStrategy(
107
ReactDefaultBatchingStrategy
108
);
src/browser/ui/ReactInjection.js
+2
@@ -23,6 +23,7 @@ var EventPluginHub = require('EventPluginHub');
23
var ReactComponent = require('ReactComponent');
24
var ReactCompositeComponent = require('ReactCompositeComponent');
25
var ReactDOM = require('ReactDOM');
26
+var ReactEmptyComponent = require('ReactEmptyComponent');
27
var ReactEventEmitter = require('ReactEventEmitter');
28
var ReactPerf = require('ReactPerf');
29
var ReactRootIndex = require('ReactRootIndex');
@@ -32,6 +33,7 @@ var ReactInjection = {
33
Component: ReactComponent.injection,
34
CompositeComponent: ReactCompositeComponent.injection,
35
DOMProperty: DOMProperty.injection,
36
+ EmptyComponent: ReactEmptyComponent.injection,
37
EventPluginHub: EventPluginHub.injection,
38
DOM: ReactDOM.injection,
39
EventEmitter: ReactEventEmitter.injection,
src/core/ReactCompositeComponent.js
+8
-1
@@ -22,6 +22,7 @@ var ReactComponent = require('ReactComponent');
22
var ReactContext = require('ReactContext');
23
var ReactCurrentOwner = require('ReactCurrentOwner');
24
var ReactDescriptor = require('ReactDescriptor');
25
+var ReactEmptyComponent = require('ReactEmptyComponent');
26
var ReactErrorUtils = require('ReactErrorUtils');
27
var ReactOwner = require('ReactOwner');
28
var ReactPerf = require('ReactPerf');
@@ -1167,6 +1168,12 @@ var ReactCompositeComponentMixin = {
1168
ReactCurrentOwner.current = this;
1169
try {
1170
renderedComponent = this.render();
1171
+ if (renderedComponent === null || renderedComponent === false) {
1172
+ renderedComponent = ReactEmptyComponent.getEmptyComponent();
1173
+ ReactEmptyComponent.registerNullComponentID(this._rootNodeID);
1174
+ } else {
1175
+ ReactEmptyComponent.deregisterNullComponentID(this._rootNodeID);
1176
+ }
1177
} finally {
1178
ReactContext.current = previousContext;
1179
ReactCurrentOwner.current = null;
@@ -1174,7 +1181,7 @@ var ReactCompositeComponentMixin = {
1181
invariant(
1182
ReactDescriptor.isValidDescriptor(renderedComponent),
1183
'%s.render(): A valid ReactComponent must be returned. You may have ' +
1177
- 'returned null, undefined, an array, or some other invalid object.',
1184
+ 'returned an array or some other invalid object.',
1185
this.constructor.displayName || 'ReactCompositeComponent'
1186
);
1187
return renderedComponent;
src/core/ReactEmptyComponent.js
new
+78
@@ -0,0 +1,78 @@
1
+/**
2
+ * Copyright 2014 Facebook, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * @providesModule ReactEmptyComponent
17
+ */
18
+
19
+"use strict";
20
+
21
+var invariant = require('invariant');
22
+
23
+var component;
24
+// This registry keeps track of the React IDs of the components that rendered to
25
+// `null` (in reality a placeholder such as `noscript`)
26
+var nullComponentIdsRegistry = {};
27
+
28
+var ReactEmptyComponentInjection = {
29
+ injectEmptyComponent: function(emptyComponent) {
30
+ component = emptyComponent;
31
+ }
32
+};
33
+
34
+/**
35
+ * @return {ReactComponent} component The injected empty component.
36
+ */
37
+function getEmptyComponent() {
38
+ invariant(
39
+ component,
40
+ 'Trying to return null from a render, but no null placeholder component ' +
41
+ 'was injected.'
42
+ );
43
+ return component();
44
+}
45
+
46
+/**
47
+ * Mark the component as having rendered to null.
48
+ * @param {string} id Component's `_rootNodeID`.
49
+ */
50
+function registerNullComponentID(id) {
51
+ nullComponentIdsRegistry[id] = true;
52
+}
53
+
54
+/**
55
+ * Unmark the component as having rendered to null: it renders to something now.
56
+ * @param {string} id Component's `_rootNodeID`.
57
+ */
58
+function deregisterNullComponentID(id) {
59
+ delete nullComponentIdsRegistry[id];
60
+}
61
+
62
+/**
63
+ * @param {string} id Component's `_rootNodeID`.
64
+ * @return {boolean} True if the component is rendered to null.
65
+ */
66
+function isNullComponentID(id) {
67
+ return nullComponentIdsRegistry[id];
68
+}
69
+
70
+var ReactEmptyComponent = {
71
+ deregisterNullComponentID: deregisterNullComponentID,
72
+ getEmptyComponent: getEmptyComponent,
73
+ injection: ReactEmptyComponentInjection,
74
+ isNullComponentID: isNullComponentID,
75
+ registerNullComponentID: registerNullComponentID
76
+};
77
+
78
+module.exports = ReactEmptyComponent;
src/core/__tests__/ReactCompositeComponent-test.js
+177
-3
@@ -19,21 +19,24 @@
19
20
"use strict";
21
22
-var MorphingComponent;
22
var ChildUpdates;
23
+var MorphingComponent;
24
var React;
25
var ReactComponent;
26
var ReactCurrentOwner;
27
+var ReactDoNotBindDeprecated;
28
+var ReactMount;
29
var ReactPropTypes;
30
+var ReactServerRendering;
31
var ReactTestUtils;
29
-var ReactMount;
30
-var ReactDoNotBindDeprecated;
32
+var TogglingComponent;
33
34
var cx;
35
var reactComponentExpect;
36
var mocks;
37
var warn;
38
39
+
40
describe('ReactCompositeComponent', function() {
41
42
beforeEach(function() {
@@ -48,6 +51,7 @@ describe('ReactCompositeComponent', function() {
51
ReactPropTypes = require('ReactPropTypes');
52
ReactTestUtils = require('ReactTestUtils');
53
ReactMount = require('ReactMount');
54
+ ReactServerRendering = require('ReactServerRendering');
55
56
MorphingComponent = React.createClass({
57
getInitialState: function() {
@@ -82,6 +86,22 @@ describe('ReactCompositeComponent', function() {
86
}
87
});
88
89
+ TogglingComponent = React.createClass({
90
+ getInitialState: function() {
91
+ return {component: this.props.firstComponent};
92
+ },
93
+ componentDidMount: function() {
94
+ console.log(this.getDOMNode());
95
+ this.setState({component: this.props.secondComponent});
96
+ },
97
+ componentDidUpdate: function() {
98
+ console.log(this.getDOMNode());
99
+ },
100
+ render: function() {
101
+ return this.state.component ? this.state.component() : null;
102
+ }
103
+ });
104
+
105
warn = console.warn;
106
console.warn = mocks.getMockFunction();
107
});
@@ -109,6 +129,160 @@ describe('ReactCompositeComponent', function() {
129
.toBeDOMComponentWithTag('a');
130
});
131
132
+ it('should render null and false as a script tag under the hood', () => {
133
+ var Component1 = React.createClass({
134
+ render: function() {
135
+ return null;
136
+ }
137
+ });
138
+ var Component2 = React.createClass({
139
+ render: function() {
140
+ return false;
141
+ }
142
+ });
143
+
144
+ var instance1 = ReactTestUtils.renderIntoDocument(<Component1 />);
145
+ var instance2 = ReactTestUtils.renderIntoDocument(<Component2 />);
146
+ reactComponentExpect(instance1)
147
+ .expectRenderedChild()
148
+ .toBeDOMComponentWithTag('script');
149
+ reactComponentExpect(instance2)
150
+ .expectRenderedChild()
151
+ .toBeDOMComponentWithTag('script');
152
+ });
153
+
154
+ it('should still throw when rendering to undefined', () => {
155
+ var Component = React.createClass({
156
+ render: function() {}
157
+ });
158
+ expect(function() {
159
+ ReactTestUtils.renderIntoDocument(<Component />);
160
+ }).toThrow(
161
+ 'Invariant Violation: Component.render(): A valid ReactComponent must ' +
162
+ 'be returned. You may have returned an array or some other invalid ' +
163
+ 'object.'
164
+ );
165
+ });
166
+
167
+ it('should be able to switch between rendering null and a normal tag', () => {
168
+ spyOn(console, 'log');
169
+
170
+ var instance1 =
171
+ <TogglingComponent
172
+ firstComponent={null}
173
+ secondComponent={React.DOM.div}
174
+ />;
175
+ var instance2 =
176
+ <TogglingComponent
177
+ firstComponent={React.DOM.div}
178
+ secondComponent={null}
179
+ />;
180
+
181
+ expect(function() {
182
+ ReactTestUtils.renderIntoDocument(instance1);
183
+ ReactTestUtils.renderIntoDocument(instance2);
184
+ }).not.toThrow();
185
+
186
+ expect(console.log.argsForCall.length).toBe(4);
187
+ expect(console.log.argsForCall[0][0]).toBe(null);
188
+ expect(console.log.argsForCall[1][0].tagName).toBe('DIV');
189
+ expect(console.log.argsForCall[2][0].tagName).toBe('DIV');
190
+ expect(console.log.argsForCall[3][0]).toBe(null);
191
+ });
192
+
193
+ it('should distinguish between a script placeholder and an actual script tag',
194
+ () => {
195
+ spyOn(console, 'log');
196
+
197
+ var instance1 =
198
+ <TogglingComponent
199
+ firstComponent={null}
200
+ secondComponent={React.DOM.script}
201
+ />;
202
+ var instance2 =
203
+ <TogglingComponent
204
+ firstComponent={React.DOM.script}
205
+ secondComponent={null}
206
+ />;
207
+
208
+ expect(function() {
209
+ ReactTestUtils.renderIntoDocument(instance1);
210
+ }).not.toThrow();
211
+ expect(function() {
212
+ ReactTestUtils.renderIntoDocument(instance2);
213
+ }).not.toThrow();
214
+
215
+ expect(console.log.argsForCall.length).toBe(4);
216
+ expect(console.log.argsForCall[0][0]).toBe(null);
217
+ expect(console.log.argsForCall[1][0].tagName).toBe('SCRIPT');
218
+ expect(console.log.argsForCall[2][0].tagName).toBe('SCRIPT');
219
+ expect(console.log.argsForCall[3][0]).toBe(null);
220
+ }
221
+ );
222
+
223
+ it('should have getDOMNode return null when multiple layers of composite ' +
224
+ 'components render to the same null placeholder', () => {
225
+ spyOn(console, 'log');
226
+
227
+ var GrandChild = React.createClass({
228
+ render: function() {
229
+ return null;
230
+ }
231
+ });
232
+
233
+ var Child = React.createClass({
234
+ render: function() {
235
+ return <GrandChild />;
236
+ }
237
+ });
238
+
239
+ var instance1 =
240
+ <TogglingComponent
241
+ firstComponent={React.DOM.div}
242
+ secondComponent={Child}
243
+ />;
244
+ var instance2 =
245
+ <TogglingComponent
246
+ firstComponent={Child}
247
+ secondComponent={React.DOM.div}
248
+ />;
249
+
250
+ expect(function() {
251
+ ReactTestUtils.renderIntoDocument(instance1);
252
+ }).not.toThrow();
253
+ expect(function() {
254
+ ReactTestUtils.renderIntoDocument(instance2);
255
+ }).not.toThrow();
256
+
257
+ expect(console.log.argsForCall.length).toBe(4);
258
+ expect(console.log.argsForCall[0][0].tagName).toBe('DIV');
259
+ expect(console.log.argsForCall[1][0]).toBe(null);
260
+ expect(console.log.argsForCall[2][0]).toBe(null);
261
+ expect(console.log.argsForCall[3][0].tagName).toBe('DIV');
262
+ }
263
+ );
264
+
265
+ it('should not thrash a server rendered layout with client side one', () => {
266
+ var Child = React.createClass({
267
+ render: function() {
268
+ return null;
269
+ }
270
+ });
271
+ var Parent = React.createClass({
272
+ render: function() {
273
+ return <div><Child /></div>;
274
+ }
275
+ });
276
+
277
+ var markup = ReactServerRendering.renderComponentToString(<Parent />);
278
+ var container = document.createElement('div');
279
+ container.innerHTML = markup;
280
+
281
+ spyOn(console, 'warn');
282
+ React.renderComponent(<Parent />, container);
283
+ expect(console.warn).not.toHaveBeenCalled();
284
+ });
285
+
286
it('should react to state changes from callbacks', function() {
287
var instance = <MorphingComponent />;
288
instance = ReactTestUtils.renderIntoDocument(instance);