Skip to content

Commit 34f8462

Browse files
committed
Save and restore host context when pushing and popping portals
1 parent be51082 commit 34f8462

File tree

8 files changed

+95
-17
lines changed

8 files changed

+95
-17
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
492492
* should render one portal
493493
* should render many portals
494494
* should render nested portals
495+
* should not apply SVG mode across portals
495496
* should pass portal context when rendering subtree elsewhere
496497
* should update portal context if it changes due to setState
497498
* should update portal context if it changes due to re-render

src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,42 @@ describe('ReactDOMFiber', () => {
333333
expect(container.innerHTML).toBe('');
334334
});
335335

336+
it('should not apply SVG mode across portals', () => {
337+
var portalContainer = document.createElement('div');
338+
339+
ReactDOM.render(
340+
<svg>
341+
<image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
342+
{ReactDOM.unstable_createPortal(
343+
<div>portal</div>,
344+
portalContainer
345+
)}
346+
<image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
347+
</svg>,
348+
container
349+
);
350+
351+
const div = portalContainer.childNodes[0];
352+
const image1 = container.firstChild.childNodes[0];
353+
const image2 = container.firstChild.childNodes[1];
354+
expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
355+
expect(div.tagName).toBe('DIV');
356+
expect(image1.namespaceURI).toBe('http://www.w3.org/2000/svg');
357+
expect(image1.tagName).toBe('image');
358+
expect(
359+
image1.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
360+
).toBe('http://i.imgur.com/w7GCRPb.png');
361+
expect(image2.namespaceURI).toBe('http://www.w3.org/2000/svg');
362+
expect(image2.tagName).toBe('image');
363+
expect(
364+
image2.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
365+
).toBe('http://i.imgur.com/w7GCRPb.png');
366+
367+
ReactDOM.unmountComponentAtNode(container);
368+
expect(portalContainer.innerHTML).toBe('');
369+
expect(container.innerHTML).toBe('');
370+
});
371+
336372
it('should pass portal context when rendering subtree elsewhere', () => {
337373
var portalContainer = document.createElement('div');
338374

src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,26 @@ describe('ReactDOMSVG', () => {
5555
node
5656
);
5757
// SVG tagName is case sensitive.
58-
expect(g.tagName).toBe('g');
5958
expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg');
59+
expect(g.tagName).toBe('g');
6060
expect(g.getAttribute('stroke-width')).toBe('5');
61-
expect(image.tagName).toBe('image');
6261
expect(image.namespaceURI).toBe('http://www.w3.org/2000/svg');
62+
expect(image.tagName).toBe('image');
6363
expect(
6464
image.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
6565
).toBe('http://i.imgur.com/w7GCRPb.png');
66-
expect(image2.tagName).toBe('image');
6766
expect(image2.namespaceURI).toBe('http://www.w3.org/2000/svg');
67+
expect(image2.tagName).toBe('image');
6868
expect(
6969
image2.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
7070
).toBe('http://i.imgur.com/w7GCRPb.png');
7171
// DOM tagName is capitalized by browsers.
72-
expect(p.tagName).toBe('P');
7372
expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
74-
expect(div.tagName).toBe('DIV');
73+
expect(p.tagName).toBe('P');
7574
expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
76-
expect(foreignDiv.tagName).toBe('DIV');
75+
expect(div.tagName).toBe('DIV');
7776
expect(foreignDiv.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
77+
expect(foreignDiv.tagName).toBe('DIV');
7878
});
7979

8080
});

src/renderers/shared/fiber/ReactFiber.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ exports.createFiberFromPortal = function(portal : ReactPortal, priorityLevel : P
343343
fiber.stateNode = {
344344
containerInfo: portal.containerInfo,
345345
implementation: portal.implementation,
346+
savedHostContext: null,
346347
};
347348
return fiber;
348349
};

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ module.exports = function<T, P, I, TI, C>(
7676
getHostParentOnStack,
7777
pushHostContainer,
7878
pushHostParent,
79+
saveHostContextToPortal,
7980
} = hostContext;
8081

8182
const {
@@ -430,7 +431,10 @@ module.exports = function<T, P, I, TI, C>(
430431
pushContextProvider(workInProgress, false);
431432
} else if (workInProgress.tag === HostContainer) {
432433
pushHostContainer(workInProgress.stateNode.containerInfo);
434+
} else if (workInProgress.tag === Portal) {
435+
saveHostContextToPortal(workInProgress);
433436
}
437+
// TODO: this is annoyingly duplicating non-jump codepaths.
434438

435439
return workInProgress.child;
436440
}
@@ -526,9 +530,8 @@ module.exports = function<T, P, I, TI, C>(
526530
// next one immediately.
527531
return null;
528532
case Portal:
529-
// TODO: host stack.
533+
saveHostContextToPortal(workInProgress);
530534
updatePortalComponent(current, workInProgress);
531-
// TODO: is this right?
532535
return workInProgress.child;
533536
case Fragment:
534537
updateFragment(current, workInProgress);

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module.exports = function<T, P, I, TI, C>(
5858
getHostContainerOnStack,
5959
popHostContainer,
6060
popHostParent,
61+
restoreHostContextFromPortal,
6162
} = hostContext;
6263

6364
function markUpdate(workInProgress : Fiber) {
@@ -255,6 +256,7 @@ module.exports = function<T, P, I, TI, C>(
255256
// TODO: Only mark this as an update if we have any pending callbacks.
256257
markUpdate(workInProgress);
257258
workInProgress.memoizedProps = workInProgress.pendingProps;
259+
restoreHostContextFromPortal(workInProgress);
258260
return null;
259261

260262
// Error cases

src/renderers/shared/fiber/ReactFiberHostContext.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
'use strict';
1414

15+
import type { Fiber } from 'ReactFiber';
16+
1517
export type HostContext<I, C> = {
1618
getHostParentOnStack() : I | null,
1719
pushHostParent(instance : I) : void,
@@ -22,20 +24,20 @@ export type HostContext<I, C> = {
2224
pushHostContainer(instance : I | C) : void,
2325
popHostContainer() : void,
2426

25-
resetHostStacks() : void,
27+
resetHostContext() : void,
28+
saveHostContextToPortal(portal : Fiber): void,
29+
restoreHostContextFromPortal(portal : Fiber): void,
2630
};
2731

2832
module.exports = function<I, C>() : HostContext<I, C> {
2933
// Host instances currently on the stack that have not yet been committed.
30-
const parentStack : Array<I | null> = [];
34+
let parentStack : Array<I | null> = [];
3135
let parentIndex = -1;
3236

3337
// Container instances currently on the stack (e.g. DOM uses this for SVG).
34-
const containerStack : Array<C | I | null> = [];
38+
let containerStack : Array<C | I | null> = [];
3539
let containerIndex = -1;
3640

37-
// TODO: this is all likely broken with portals.
38-
3941
function getHostParentOnStack() : I | null {
4042
if (parentIndex === -1) {
4143
return null;
@@ -77,11 +79,42 @@ module.exports = function<I, C>() : HostContext<I, C> {
7779
containerIndex--;
7880
}
7981

80-
function resetHostStacks() : void {
82+
function resetHostContext() : void {
8183
parentIndex = -1;
8284
containerIndex = -1;
8385
}
8486

87+
function saveHostContextToPortal(portal : Fiber) {
88+
const stateNode = portal.stateNode;
89+
// We don't throw if it already exists here because it might exist
90+
// if something inside threw, and we started from the top.
91+
// TODO: add tests for error boundaries inside portals when both are stable.
92+
stateNode.savedHostContext = {
93+
containerStack,
94+
containerIndex,
95+
parentStack,
96+
parentIndex,
97+
};
98+
containerStack = [];
99+
containerIndex = -1;
100+
parentStack = [];
101+
parentIndex = -1;
102+
pushHostContainer(stateNode.containerInfo);
103+
}
104+
105+
function restoreHostContextFromPortal(portal : Fiber) {
106+
const stateNode = portal.stateNode;
107+
const savedHostContext = stateNode.savedHostContext;
108+
stateNode.savedHostContext = null;
109+
if (savedHostContext == null) {
110+
throw new Error('A portal has no host context saved on it.');
111+
}
112+
containerStack = savedHostContext.containerStack;
113+
containerIndex = savedHostContext.containerIndex;
114+
parentStack = savedHostContext.parentStack;
115+
parentIndex = savedHostContext.parentIndex;
116+
}
117+
85118
return {
86119
getHostParentOnStack,
87120
pushHostParent,
@@ -92,6 +125,8 @@ module.exports = function<I, C>() : HostContext<I, C> {
92125
pushHostContainer,
93126
popHostContainer,
94127

95-
resetHostStacks,
128+
resetHostContext,
129+
saveHostContextToPortal,
130+
restoreHostContextFromPortal,
96131
};
97132
};

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ type TrappedError = {
6767

6868
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
6969
const hostContext = ReactFiberHostContext();
70-
const { resetHostStacks } = hostContext;
70+
const { resetHostContext } = hostContext;
7171
const { beginWork } = ReactFiberBeginWork(config, hostContext, scheduleUpdate);
7272
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
7373
const {
@@ -222,7 +222,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
222222
}
223223

224224
resetAfterCommit();
225-
resetHostStacks();
225+
resetHostContext();
226226

227227
// Next, we'll perform all life-cycles and ref callbacks. Life-cycles
228228
// happens as a separate pass so that all effects in the entire tree have

0 commit comments

Comments
 (0)