@samitouri / QOS-React / commits / 6434c37649

[Fiber] Add ReactDOMFiber.unstable_createPortal() (#8386)

* [Fiber] Add ReactDOMFiber.unstable_createPortal() While #8368 added a version of `ReactDOM.unstable_renderSubtreeIntoContainer()` to Fiber, it is a bit hacky and, more importantly, incompatible with Fiber goals. Since it encourages performing portal work in lifecycles, it stretches the commit phase and prevents slicing that work, potentially negating Fiber benefits. This PR adds a first version of a declarative API meant to replace `ReactDOM.unstable_renderSubtreeIntoContainer()`. The API is a declarative way to render subtrees into DOM node containers. * Remove hacks related to output field

Dan Abramov committed Nov 22, 2016 at 22:08 UTC 6434c3764911a3c9b67bb6391b0d2516d803cc67
11 files changed +429 -2
scripts/fiber/tests-passing.txt
+4
@@ -488,6 +488,10 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
488 * finds the first child when a component returns a fragment
489 * finds the first child even when fragment is nested
490 * finds the first child even when first child renders null
491 +* should render portal children
492 +* should pass portal context when rendering subtree elsewhere
493 +* should update portal context if it changes due to setState
494 +* should update portal context if it changes due to re-render
495
496 src/renderers/dom/shared/__tests__/CSSProperty-test.js
497 * should generate browser prefixes for its `isUnitlessNumber`
src/renderers/dom/fiber/ReactDOMFiber.js
+7
@@ -14,6 +14,7 @@
14
15 import type { Fiber } from 'ReactFiber';
16 import type { HostChildren } from 'ReactFiberReconciler';
17 +import type { ReactNodeList } from 'ReactTypes';
18
19 var ReactControlledComponent = require('ReactControlledComponent');
20 var ReactDOMComponentTree = require('ReactDOMComponentTree');
@@ -22,6 +23,7 @@ var ReactDOMFiberComponent = require('ReactDOMFiberComponent');
23 var ReactDOMInjection = require('ReactDOMInjection');
24 var ReactFiberReconciler = require('ReactFiberReconciler');
25 var ReactInstanceMap = require('ReactInstanceMap');
26 +var ReactPortal = require('ReactPortal');
27
28 var findDOMNode = require('findDOMNode');
29 var invariant = require('invariant');
@@ -192,6 +194,11 @@ var ReactDOM = {
194
195 findDOMNode: findDOMNode,
196
197 + unstable_createPortal(children: ReactNodeList, container : DOMContainerElement, key : ?string = null) {
198 + // TODO: pass ReactDOM portal implementation as third argument
199 + return ReactPortal.createPortal(children, container, null, key);
200 + },
201 +
202 unstable_batchedUpdates<A>(fn : () => A) : A {
203 return DOMRenderer.batchedUpdates(fn);
204 },
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
+221
@@ -186,4 +186,225 @@ describe('ReactDOMFiber', () => {
186 expect(firstNode.tagName).toBe('DIV');
187 });
188 }
189 +
190 + if (ReactDOMFeatureFlags.useFiber) {
191 + it('should render portal children', () => {
192 + var portalContainer1 = document.createElement('div');
193 + var portalContainer2 = document.createElement('div');
194 +
195 + var ops = [];
196 + class Child extends React.Component {
197 + componentDidMount() {
198 + ops.push(`${this.props.name} componentDidMount`);
199 + }
200 + componentDidUpdate() {
201 + ops.push(`${this.props.name} componentDidUpdate`);
202 + }
203 + componentWillUnmount() {
204 + ops.push(`${this.props.name} componentWillUnmount`);
205 + }
206 + render() {
207 + return <div>{this.props.name}</div>;
208 + }
209 + }
210 +
211 + class Parent extends React.Component {
212 + componentDidMount() {
213 + ops.push(`Parent:${this.props.step} componentDidMount`);
214 + }
215 + componentDidUpdate() {
216 + ops.push(`Parent:${this.props.step} componentDidUpdate`);
217 + }
218 + componentWillUnmount() {
219 + ops.push(`Parent:${this.props.step} componentWillUnmount`);
220 + }
221 + render() {
222 + const {step} = this.props;
223 + return [
224 + <Child name={`normal[0]:${step}`} />,
225 + ReactDOM.unstable_createPortal(
226 + <Child name={`portal1[0]:${step}`} />,
227 + portalContainer1
228 + ),
229 + <Child name={`normal[1]:${step}`} />,
230 + ReactDOM.unstable_createPortal(
231 + [
232 + <Child name={`portal2[0]:${step}`} />,
233 + <Child name={`portal2[1]:${step}`} />,
234 + ],
235 + portalContainer2
236 + ),
237 + ];
238 + }
239 + }
240 +
241 + ReactDOM.render(<Parent step="a" />, container);
242 + expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:a</div>');
243 + expect(portalContainer2.innerHTML).toBe('<div>portal2[0]:a</div><div>portal2[1]:a</div>');
244 + expect(container.innerHTML).toBe('<div>normal[0]:a</div><div>normal[1]:a</div>');
245 + expect(ops).toEqual([
246 + 'normal[0]:a componentDidMount',
247 + 'portal1[0]:a componentDidMount',
248 + 'normal[1]:a componentDidMount',
249 + 'portal2[0]:a componentDidMount',
250 + 'portal2[1]:a componentDidMount',
251 + 'Parent:a componentDidMount',
252 + ]);
253 +
254 + ops.length = 0;
255 + ReactDOM.render(<Parent step="b" />, container);
256 + expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:b</div>');
257 + expect(portalContainer2.innerHTML).toBe('<div>portal2[0]:b</div><div>portal2[1]:b</div>');
258 + expect(container.innerHTML).toBe('<div>normal[0]:b</div><div>normal[1]:b</div>');
259 + expect(ops).toEqual([
260 + 'normal[0]:b componentDidUpdate',
261 + 'portal1[0]:b componentDidUpdate',
262 + 'normal[1]:b componentDidUpdate',
263 + 'portal2[0]:b componentDidUpdate',
264 + 'portal2[1]:b componentDidUpdate',
265 + 'Parent:b componentDidUpdate',
266 + ]);
267 +
268 + ops.length = 0;
269 + ReactDOM.unmountComponentAtNode(container);
270 + expect(portalContainer1.innerHTML).toBe('');
271 + expect(portalContainer2.innerHTML).toBe('');
272 + expect(container.innerHTML).toBe('');
273 + expect(ops).toEqual([
274 + 'Parent:b componentWillUnmount',
275 + 'normal[0]:b componentWillUnmount',
276 + 'portal1[0]:b componentWillUnmount',
277 + 'normal[1]:b componentWillUnmount',
278 + 'portal2[0]:b componentWillUnmount',
279 + 'portal2[1]:b componentWillUnmount',
280 + ]);
281 + });
282 +
283 + it('should pass portal context when rendering subtree elsewhere', () => {
284 + var portalContainer = document.createElement('div');
285 +
286 + class Component extends React.Component {
287 + static contextTypes = {
288 + foo: React.PropTypes.string.isRequired,
289 + };
290 +
291 + render() {
292 + return <div>{this.context.foo}</div>;
293 + }
294 + }
295 +
296 + class Parent extends React.Component {
297 + static childContextTypes = {
298 + foo: React.PropTypes.string.isRequired,
299 + };
300 +
301 + getChildContext() {
302 + return {
303 + foo: 'bar',
304 + };
305 + }
306 +
307 + render() {
308 + return ReactDOM.unstable_createPortal(
309 + <Component />,
310 + portalContainer
311 + );
312 + }
313 + }
314 +
315 + ReactDOM.render(<Parent />, container);
316 + expect(container.innerHTML).toBe('');
317 + expect(portalContainer.innerHTML).toBe('<div>bar</div>');
318 + });
319 +
320 + it('should update portal context if it changes due to setState', () => {
321 + var portalContainer = document.createElement('div');
322 +
323 + class Component extends React.Component {
324 + static contextTypes = {
325 + foo: React.PropTypes.string.isRequired,
326 + getFoo: React.PropTypes.func.isRequired,
327 + };
328 +
329 + render() {
330 + return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
331 + }
332 + }
333 +
334 + class Parent extends React.Component {
335 + static childContextTypes = {
336 + foo: React.PropTypes.string.isRequired,
337 + getFoo: React.PropTypes.func.isRequired,
338 + };
339 +
340 + state = {
341 + bar: 'initial',
342 + };
343 +
344 + getChildContext() {
345 + return {
346 + foo: this.state.bar,
347 + getFoo: () => this.state.bar,
348 + };
349 + }
350 +
351 + render() {
352 + return ReactDOM.unstable_createPortal(
353 + <Component />,
354 + portalContainer
355 + );
356 + }
357 + }
358 +
359 + var instance = ReactDOM.render(<Parent />, container);
360 + expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
361 + expect(container.innerHTML).toBe('');
362 + instance.setState({bar: 'changed'});
363 + expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
364 + expect(container.innerHTML).toBe('');
365 + });
366 +
367 + it('should update portal context if it changes due to re-render', () => {
368 + var portalContainer = document.createElement('div');
369 +
370 + class Component extends React.Component {
371 + static contextTypes = {
372 + foo: React.PropTypes.string.isRequired,
373 + getFoo: React.PropTypes.func.isRequired,
374 + };
375 +
376 + render() {
377 + return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
378 + }
379 + }
380 +
381 + class Parent extends React.Component {
382 + static childContextTypes = {
383 + foo: React.PropTypes.string.isRequired,
384 + getFoo: React.PropTypes.func.isRequired,
385 + };
386 +
387 + getChildContext() {
388 + return {
389 + foo: this.props.bar,
390 + getFoo: () => this.props.bar,
391 + };
392 + }
393 +
394 + render() {
395 + return ReactDOM.unstable_createPortal(
396 + <Component />,
397 + portalContainer
398 + );
399 + }
400 + }
401 +
402 + ReactDOM.render(<Parent bar="initial" />, container);
403 + expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
404 + expect(container.innerHTML).toBe('');
405 + ReactDOM.render(<Parent bar="changed" />, container);
406 + expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
407 + expect(container.innerHTML).toBe('');
408 + });
409 + }
410 });
src/renderers/shared/fiber/ReactChildFiber.js
+89
@@ -13,6 +13,7 @@
13 'use strict';
14
15 import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
16 +import type { ReactPortal } from 'ReactPortal';
17 import type { Fiber } from 'ReactFiber';
18 import type { PriorityLevel } from 'ReactPriorityLevel';
19
@@ -21,6 +22,9 @@ var {
22 REACT_COROUTINE_TYPE,
23 REACT_YIELD_TYPE,
24 } = require('ReactCoroutine');
25 +var {
26 + REACT_PORTAL_TYPE,
27 +} = require('ReactPortal');
28
29 var ReactFiber = require('ReactFiber');
30 var ReactReifiedYield = require('ReactReifiedYield');
@@ -37,6 +41,7 @@ const {
41 createFiberFromText,
42 createFiberFromCoroutine,
43 createFiberFromYield,
44 + createFiberFromPortal,
45 } = ReactFiber;
46
47 const {
@@ -52,6 +57,7 @@ const {
57 CoroutineComponent,
58 YieldComponent,
59 Fragment,
60 + Portal,
61 } = ReactTypeOfWork;
62
63 const {
@@ -303,6 +309,31 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
309 }
310 }
311
312 + function updatePortal(
313 + returnFiber : Fiber,
314 + current : ?Fiber,
315 + portal : ReactPortal,
316 + priority : PriorityLevel
317 + ) : Fiber {
318 + if (
319 + current == null ||
320 + current.tag !== Portal ||
321 + current.stateNode.containerInfo !== portal.containerInfo ||
322 + current.stateNode.implementation !== portal.implementation
323 + ) {
324 + // Insert
325 + const created = createFiberFromPortal(portal, priority);
326 + created.return = returnFiber;
327 + return created;
328 + } else {
329 + // Update
330 + const existing = useFiber(current, priority);
331 + existing.pendingProps = portal.children;
332 + existing.return = returnFiber;
333 + return existing;
334 + }
335 + }
336 +
337 function updateFragment(
338 returnFiber : Fiber,
339 current : ?Fiber,
@@ -359,6 +390,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
390 created.return = returnFiber;
391 return created;
392 }
393 +
394 + case REACT_PORTAL_TYPE: {
395 + const created = createFiberFromPortal(newChild, priority);
396 + created.return = returnFiber;
397 + return created;
398 + }
399 }
400
401 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -468,6 +505,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
505 ) || null;
506 return updateYield(returnFiber, matchedFiber, newChild, priority);
507 }
508 +
509 + case REACT_PORTAL_TYPE: {
510 + const matchedFiber = existingChildren.get(
511 + newChild.key === null ? newIdx : newChild.key
512 + ) || null;
513 + return updatePortal(returnFiber, matchedFiber, newChild, priority);
514 + }
515 }
516
517 if (isArray(newChild) || getIteratorFn(newChild)) {
@@ -769,6 +813,43 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
813 return created;
814 }
815
816 + function reconcileSinglePortal(
817 + returnFiber : Fiber,
818 + currentFirstChild : ?Fiber,
819 + portal : ReactPortal,
820 + priority : PriorityLevel
821 + ) : Fiber {
822 + const key = portal.key;
823 + let child = currentFirstChild;
824 + while (child) {
825 + // TODO: If key === null and child.key === null, then this only applies to
826 + // the first item in the list.
827 + if (child.key === key) {
828 + if (
829 + child.tag === Portal &&
830 + child.stateNode.containerInfo === portal.containerInfo &&
831 + child.stateNode.implementation === portal.implementation
832 + ) {
833 + deleteRemainingChildren(returnFiber, child.sibling);
834 + const existing = useFiber(child, priority);
835 + existing.pendingProps = portal.children;
836 + existing.return = returnFiber;
837 + return existing;
838 + } else {
839 + deleteRemainingChildren(returnFiber, child);
840 + break;
841 + }
842 + } else {
843 + deleteChild(returnFiber, child);
844 + }
845 + child = child.sibling;
846 + }
847 +
848 + const created = createFiberFromPortal(portal, priority);
849 + created.return = returnFiber;
850 + return created;
851 + }
852 +
853 // This API will tag the children with the side-effect of the reconciliation
854 // itself. They will be added to the side-effect list as we pass through the
855 // children and the parent.
@@ -817,6 +898,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
898 newChild,
899 priority
900 ));
901 +
902 + case REACT_PORTAL_TYPE:
903 + return placeSingleChild(reconcileSinglePortal(
904 + returnFiber,
905 + currentFirstChild,
906 + newChild,
907 + priority
908 + ));
909 }
910
911 if (isArray(newChild)) {
src/renderers/shared/fiber/ReactFiber.js
+13
@@ -14,6 +14,7 @@
14
15 import type { ReactFragment } from 'ReactTypes';
16 import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
17 +import type { ReactPortal } from 'ReactPortal';
18 import type { TypeOfWork } from 'ReactTypeOfWork';
19 import type { TypeOfSideEffect } from 'ReactTypeOfSideEffect';
20 import type { PriorityLevel } from 'ReactPriorityLevel';
@@ -29,6 +30,7 @@ var {
30 CoroutineComponent,
31 YieldComponent,
32 Fragment,
33 + Portal,
34 } = ReactTypeOfWork;
35
36 var {
@@ -338,3 +340,14 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
340 fiber.pendingProps = {};
341 return fiber;
342 };
343 +
344 +exports.createFiberFromPortal = function(portal : ReactPortal, priorityLevel : PriorityLevel) : Fiber {
345 + const fiber = createFiber(Portal, portal.key);
346 + fiber.pendingProps = portal.children;
347 + fiber.pendingWorkPriority = priorityLevel;
348 + fiber.stateNode = {
349 + containerInfo: portal.containerInfo,
350 + implementation: portal.implementation,
351 + };
352 + return fiber;
353 +};
src/renderers/shared/fiber/ReactFiberBeginWork.js
+9
@@ -45,6 +45,7 @@ var {
45 CoroutineHandlerPhase,
46 YieldComponent,
47 Fragment,
48 + Portal,
49 } = ReactTypeOfWork;
50 var {
51 NoWork,
@@ -298,6 +299,10 @@ module.exports = function<T, P, I, TI, C>(
299 reconcileChildren(current, workInProgress, coroutine.children);
300 }
301
302 + function updatePortalComponent(current, workInProgress) {
303 + reconcileChildren(current, workInProgress, workInProgress.pendingProps);
304 + }
305 +
306 /*
307 function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
308 let child = firstChild;
@@ -450,6 +455,10 @@ module.exports = function<T, P, I, TI, C>(
455 // A yield component is just a placeholder, we can just run through the
456 // next one immediately.
457 return null;
458 + case Portal:
459 + updatePortalComponent(current, workInProgress);
460 + // TODO: is this right?
461 + return workInProgress.child;
462 case Fragment:
463 updateFragment(current, workInProgress);
464 return workInProgress.child;
src/renderers/shared/fiber/ReactFiberCommitWork.js
+16
@@ -22,6 +22,7 @@ var {
22 HostContainer,
23 HostComponent,
24 HostText,
25 + Portal,
26 } = ReactTypeOfWork;
27 var { callCallbacks } = require('ReactFiberUpdateQueue');
28
@@ -253,6 +254,11 @@ module.exports = function<T, P, I, TI, C>(
254 detachRef(current);
255 return;
256 }
257 + case Portal: {
258 + const containerInfo : C = current.stateNode.containerInfo;
259 + updateContainer(containerInfo, null);
260 + return;
261 + }
262 }
263 }
264
@@ -291,6 +297,12 @@ module.exports = function<T, P, I, TI, C>(
297 commitTextUpdate(textInstance, oldText, newText);
298 return;
299 }
300 + case Portal: {
301 + const children = finishedWork.child;
302 + const containerInfo : C = finishedWork.stateNode.containerInfo;
303 + updateContainer(containerInfo, children);
304 + return;
305 + }
306 default:
307 throw new Error('This unit of work tag should not have side-effects.');
308 }
@@ -353,6 +365,10 @@ module.exports = function<T, P, I, TI, C>(
365 // We have no life-cycles associated with text.
366 return;
367 }
368 + case Portal: {
369 + // We have no life-cycles associated with portals.
370 + return;
371 + }
372 default:
373 throw new Error('This unit of work tag should not have side-effects.');
374 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+6
@@ -36,6 +36,7 @@ var {
36 CoroutineHandlerPhase,
37 YieldComponent,
38 Fragment,
39 + Portal,
40 } = ReactTypeOfWork;
41 var {
42 Update,
@@ -251,6 +252,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
252 case Fragment:
253 transferOutput(workInProgress.child, workInProgress);
254 return null;
255 + case Portal:
256 + markUpdate(workInProgress);
257 + workInProgress.output = null;
258 + workInProgress.memoizedProps = workInProgress.pendingProps;
259 + return null;
260
261 // Error cases
262 case IndeterminateComponent:
src/renderers/shared/fiber/ReactTypeOfWork.js
+2 -1
@@ -12,7 +12,7 @@
12
13 'use strict';
14
15 -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
15 +export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
16
17 module.exports = {
18 IndeterminateComponent: 0, // Before we know whether it is functional or class
@@ -25,4 +25,5 @@ module.exports = {
25 CoroutineHandlerPhase: 7,
26 YieldComponent: 8,
27 Fragment: 9,
28 + Portal: 10, // A subtree. Could be an entry point to a different renderer.
29 };
src/renderers/shared/fiber/isomorphic/ReactPortal.js new
+60
@@ -0,0 +1,60 @@
1 +/**
2 + * Copyright 2014-present, 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 + * @providesModule ReactPortal
10 + * @flow
11 + */
12 +
13 +'use strict';
14 +
15 +import type { ReactNodeList } from 'ReactTypes';
16 +
17 +// The Symbol used to tag the special React types. If there is no native Symbol
18 +// nor polyfill, then a plain number is used for performance.
19 +var REACT_PORTAL_TYPE =
20 + (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.portal')) ||
21 + 0xeaca;
22 +
23 +export type ReactPortal = {
24 + $$typeof: Symbol | number,
25 + key: null | string,
26 + containerInfo: any,
27 + children : ReactNodeList,
28 + // TODO: figure out the API for cross-renderer implementation.
29 + implementation: any,
30 +};
31 +
32 +exports.createPortal = function(
33 + children : ReactNodeList,
34 + containerInfo : any,
35 + // TODO: figure out the API for cross-renderer implementation.
36 + implementation: any,
37 + key : ?string = null
38 +) : ReactPortal {
39 + return {
40 + // This tag allow us to uniquely identify this as a React Portal
41 + $$typeof: REACT_PORTAL_TYPE,
42 + key: key == null ? null : '' + key,
43 + children,
44 + containerInfo,
45 + implementation,
46 + };
47 +};
48 +
49 +/**
50 + * Verifies the object is a portal object.
51 + */
52 +exports.isPortal = function(object : mixed) : boolean {
53 + return (
54 + typeof object === 'object' &&
55 + object !== null &&
56 + object.$$typeof === REACT_PORTAL_TYPE
57 + );
58 +};
59 +
60 +exports.REACT_PORTAL_TYPE = REACT_PORTAL_TYPE;
src/renderers/shared/fiber/isomorphic/ReactTypes.js
+2 -1
@@ -13,8 +13,9 @@
13 'use strict';
14
15 import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
16 +import type { ReactPortal } from 'ReactPortal';
17
17 -export type ReactNode = ReactElement<any> | ReactCoroutine | ReactYield | ReactText | ReactFragment;
18 +export type ReactNode = ReactElement<any> | ReactCoroutine | ReactYield | ReactPortal | ReactText | ReactFragment;
19
20 export type ReactFragment = ReactEmpty | Iterable<ReactNode>;
21