@samitouri / QOS-React-2 / commits / f0f59877aa

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);