diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index c4c851d8e33f8..1a9bc4a06af8b 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -15,6 +15,7 @@ import type { Fiber } from 'ReactFiber'; import type { ReactNodeList } from 'ReactTypes'; +var DOMNamespaces = require('DOMNamespaces'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactControlledComponent = require('ReactControlledComponent'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); @@ -37,6 +38,10 @@ var { updateProperties, } = ReactDOMFiberComponent; var { precacheFiberNode } = ReactDOMComponentTree; +var { + svg: SVG_NAMESPACE, + mathml: MATH_NAMESPACE, +} = DOMNamespaces; const DOCUMENT_NODE = 9; @@ -58,8 +63,184 @@ type TextInstance = Text; let eventsEnabled : ?boolean = null; let selectionInformation : ?mixed = null; +// The next few variables are mutable state that changes as we perform work. +// We could have replaced them by a single stack of namespaces but we want +// to avoid it since this is a very hot path. +let currentNamespaceURI : null | SVG_NAMESPACE | MATH_NAMESPACE = null; +// How many s we have entered so far. +// We increment and decrement it when pushing and popping . +// We use this counter as the current index for accessing the array below. +let foreignObjectDepth : number = 0; +// How many s have we entered so far. +// We increment or decrement the last array item when pushing and popping . +// A new counter is appended to the end whenever we enter a . +let svgDepthByForeignObjectDepth : Array | null = null; +// For example: +// +// ^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^] +// [ 1 , 3 , 1 ] + +// The mutable state above becomes irrelevant whenever we push a portal +// because a portal represents a different DOM tree. We store a snapshot +// of this state and restore it when the portal is popped. +type PortalState = { + currentNamespaceURI: string | null, + foreignObjectDepth: number, + svgDepthByForeignObjectDepth: Array | null, +}; +let portalState : Array | null = null; +let portalStateIndex : number = -1; + +function getIntrinsicNamespaceURI(type : string) { + switch (type) { + case 'svg': + return SVG_NAMESPACE; + case 'math': + return MATH_NAMESPACE; + default: + return null; + } +} + var DOMRenderer = ReactFiberReconciler({ + pushHostContext(type : string) { + switch (type) { + case 'svg': + // Lazily initialize the array for the first time. + if (!svgDepthByForeignObjectDepth) { + svgDepthByForeignObjectDepth = [0]; + } + if (currentNamespaceURI == null) { + // We are entering an for the first time. + currentNamespaceURI = SVG_NAMESPACE; + svgDepthByForeignObjectDepth[foreignObjectDepth] = 1; + } else if (currentNamespaceURI === SVG_NAMESPACE) { + // We are entering an inside . + // We record this fact so that when we pop this , we stay in the + // SVG mode instead of switching to HTML mode. + svgDepthByForeignObjectDepth[foreignObjectDepth]++; + } + break; + case 'math': + if (currentNamespaceURI == null) { + currentNamespaceURI = MATH_NAMESPACE; + } + break; + case 'foreignObject': + if (currentNamespaceURI === SVG_NAMESPACE) { + currentNamespaceURI = null; + // We are in HTML mode again, so current nesting counter needs + // to be reset. However we still need to remember its value when we + // pop this . So instead of resetting the counter, we + // advance the pointer, and start a new independent depth + // counter at the next array index. + foreignObjectDepth++; + if (!svgDepthByForeignObjectDepth) { + throw new Error('Expected to already be in SVG mode.'); + } + svgDepthByForeignObjectDepth[foreignObjectDepth] = 0; + } + break; + } + }, + + popHostContext(type : string) { + switch (type) { + case 'svg': + if (currentNamespaceURI === SVG_NAMESPACE) { + if (!svgDepthByForeignObjectDepth) { + throw new Error('Expected to already be in SVG mode.'); + } + if (svgDepthByForeignObjectDepth[foreignObjectDepth] === 1) { + // We exited all nested nodes. + // We can switch to HTML mode. + currentNamespaceURI = null; + } else { + // There is still an above so we stay in SVG mode. + // We decrease the counter so that next time we leave + // we will be able to switch to HTML mode. + svgDepthByForeignObjectDepth[foreignObjectDepth]--; + } + } + break; + case 'math': + if (currentNamespaceURI === MATH_NAMESPACE) { + currentNamespaceURI = null; + } + break; + case 'foreignObject': + if (currentNamespaceURI == null) { + // We are exiting and nested s may exist above. + // Switch to the previous depth counter by decreasing the index. + foreignObjectDepth--; + currentNamespaceURI = SVG_NAMESPACE; + } + break; + } + }, + + pushHostPortal() : void { + // We optimize for the simple case: portals usually exist outside of SVG. + const canBailOutOfTrackingPortalState = ( + // If we're in HTML mode, we don't need to save this. + currentNamespaceURI == null && + // If state was ever saved before, we can't bail out because we wouldn't + // be able to tell whether to restore it or not next time we pop a portal. + portalStateIndex === -1 + ); + if (canBailOutOfTrackingPortalState) { + return; + } + // We are going to save the state before entering the portal. + portalStateIndex++; + // We are inside (or deeper) and need to store that before + // jumping into a portal elsewhere in the tree. + if (!portalState) { + portalState = []; + } + if (!portalState[portalStateIndex]) { + // Lazily allocate a single object for every portal nesting level. + portalState[portalStateIndex] = { + currentNamespaceURI, + foreignObjectDepth, + svgDepthByForeignObjectDepth, + }; + } else { + // If we already have state on the stack, just mutate it. + const mutableState = portalState[portalStateIndex]; + mutableState.currentNamespaceURI = currentNamespaceURI; + mutableState.foreignObjectDepth = foreignObjectDepth; + mutableState.svgDepthByForeignObjectDepth = svgDepthByForeignObjectDepth; + } + // Reset the host context we're working with. + // TODO: what if the portal is inside element itself? + // We currently don't handle this case. + currentNamespaceURI = null; + foreignObjectDepth = 0; + svgDepthByForeignObjectDepth = null; + }, + + popHostPortal() { + if (portalStateIndex === -1 || portalState == null) { + // There is nothing interesting to restore. + return; + } + // Restore to the state before we entered that portal. + const savedState = portalState[portalStateIndex]; + currentNamespaceURI = savedState.currentNamespaceURI; + foreignObjectDepth = savedState.foreignObjectDepth; + svgDepthByForeignObjectDepth = savedState.svgDepthByForeignObjectDepth; + // We have restored the state. + portalStateIndex--; + }, + + resetHostContext() : void { + currentNamespaceURI = null; + foreignObjectDepth = 0; + portalStateIndex = -1; + }, + prepareForCommit() : void { eventsEnabled = ReactBrowserEventEmitter.isEnabled(); ReactBrowserEventEmitter.setEnabled(false); @@ -80,7 +261,8 @@ var DOMRenderer = ReactFiberReconciler({ ) : Instance { const root = document.documentElement; // HACK - const domElement : Instance = createElement(type, props, root); + const namespaceURI = currentNamespaceURI || getIntrinsicNamespaceURI(type); + const domElement : Instance = createElement(type, props, namespaceURI, root); precacheFiberNode(internalInstanceHandle, domElement); return domElement; }, diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 7c6ebd9b3c65f..c40fd26aeb64d 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -15,7 +15,6 @@ 'use strict'; var CSSPropertyOperations = require('CSSPropertyOperations'); -var DOMNamespaces = require('DOMNamespaces'); var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); var EventPluginRegistry = require('EventPluginRegistry'); @@ -453,43 +452,19 @@ function updateDOMProperties( var ReactDOMFiberComponent = { - // TODO: Use this to keep track of changes to the host context and use this - // to determine whether we switch to svg and back. - // TODO: Does this need to check the current namespace? In case these tags - // happen to be valid in some other namespace. - isNewHostContainer(tag : string) { - return tag === 'svg' || tag === 'foreignobject'; - }, - createElement( tag : string, props : Object, + namespaceURI : string | null, rootContainerElement : Element ) : Element { validateDangerousTag(tag); // TODO: // tag.toLowerCase(); Do we need to apply lower case only on non-custom elements? - // We create tags in the namespace of their parent container, except HTML - // tags get no namespace. - var namespaceURI = rootContainerElement.namespaceURI; - if (namespaceURI == null || - namespaceURI === DOMNamespaces.svg && - rootContainerElement.tagName === 'foreignObject') { - namespaceURI = DOMNamespaces.html; - } - if (namespaceURI === DOMNamespaces.html) { - if (tag === 'svg') { - namespaceURI = DOMNamespaces.svg; - } else if (tag === 'math') { - namespaceURI = DOMNamespaces.mathml; - } - // TODO: Make this a new root container element. - } - var ownerDocument = rootContainerElement.ownerDocument; var domElement : Element; - if (namespaceURI === DOMNamespaces.html) { + if (namespaceURI == null) { if (tag === 'script') { // Create the script via .innerHTML so its "parser-inserted" flag is // set to true and it does not execute diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js index 13f6a41499b3e..250fcc9b3f660 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -188,6 +188,39 @@ describe('ReactDOMFiber', () => { } if (ReactDOMFeatureFlags.useFiber) { + var svgEls, htmlEls, mathEls; + var expectSVG = {ref: el => svgEls.push(el)}; + var expectHTML = {ref: el => htmlEls.push(el)}; + var expectMath = {ref: el => mathEls.push(el)}; + + var portal = function(tree) { + return ReactDOM.unstable_createPortal( + tree, + document.createElement('div') + ); + }; + + var assertNamespacesMatch = function(tree) { + container = document.createElement('div'); + svgEls = []; + htmlEls = []; + mathEls = []; + + ReactDOM.render(tree, container); + svgEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + }); + htmlEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + }); + mathEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); + }); + + ReactDOM.unmountComponentAtNode(container); + expect(container.innerHTML).toBe(''); + }; + it('should render one portal', () => { var portalContainer = document.createElement('div'); @@ -333,6 +366,255 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe(''); }); + it('should keep track of namespace across portals (simple)', () => { + assertNamespacesMatch( + + + {portal( +
+ )} + + + ); + assertNamespacesMatch( + + + {portal( +
+ )} + + + ); + assertNamespacesMatch( +
+

+ {portal( + + + + )} +

+

+ ); + }); + + it('should keep track of namespace across portals (medium)', () => { + assertNamespacesMatch( +
+ + + {portal( + + + + )} + +

+

+ ); + assertNamespacesMatch( + + + {portal( + + + +

+ + + +

+ + + + )} + + + ); + assertNamespacesMatch( +

+ {portal( + + {portal( +
+ )} + + + )} +

+

+ ); + assertNamespacesMatch( + + + {portal( +
+ )} + + + + + ); + }); + + it('should keep track of namespace across portals (complex)', () => { + assertNamespacesMatch( +
+ {portal( + + + + )} +

+ + + + + + + + + +

+

+ ); + assertNamespacesMatch( +
+ + + + {portal( + + + + + + + + )} + + +

+ {portal(

)} +

+ + + + +

+ + ); + assertNamespacesMatch( +

+ + +

+ {portal( + + + + + +

+ + {portal(

)} + + + + )} +

+ + + +

+ + ); + }); + + it('should unwind namespaces on uncaught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + expect(() => { + assertNamespacesMatch( + + + + ); + }).toThrow('Hello'); + assertNamespacesMatch( +

+ ); + }); + + it('should unwind namespaces on caught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return

; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + + + + + + + + + ); + assertNamespacesMatch( +

+ ); + }); + + it('should unwind namespaces on caught errors in a portal', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + {portal( + + ) + + )} + + + + ); + }); + it('should pass portal context when rendering subtree elsewhere', () => { var portalContainer = document.createElement('div'); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js index 30760d3ae88db..fd50a20d7ef3b 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js @@ -12,12 +12,14 @@ 'use strict'; var React; +var ReactDOM; var ReactDOMServer; describe('ReactDOMSVG', () => { beforeEach(() => { React = require('React'); + ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); }); @@ -30,4 +32,129 @@ describe('ReactDOMSVG', () => { expect(markup).toContain('xlink:href="http://i.imgur.com/w7GCRPb.png"'); }); + it('creates elements with SVG namespace inside SVG tag during mount', () => { + var node = document.createElement('div'); + var div, div2, div3, foreignObject, foreignObject2, g, image, image2, image3, p, svg, svg2, svg3, svg4; + ReactDOM.render( +
+ svg = el}> + g = el} strokeWidth="5"> + svg2 = el}> + foreignObject = el}> + svg3 = el}> + svg4 = el} /> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +
div = el} /> + + + image2 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject2 = el}> +
div2 = el} /> + + + +

p = el}> + + image3 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +

+
div3 = el} /> +
, + node + ); + [svg, svg2, svg3, svg4].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + // SVG tagName is case sensitive. + expect(el.tagName).toBe('svg'); + }); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + // DOM tagName is capitalized by browsers. + expect(p.tagName).toBe('P'); + [image, image2, image3].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('image'); + expect( + el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + }); + [foreignObject, foreignObject2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('foreignObject'); + }); + [div, div2, div3].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + expect(el.tagName).toBe('DIV'); + }); + }); + + it('creates elements with SVG namespace inside SVG tag during update', () => { + var inst, div, div2, foreignObject, foreignObject2, g, image, image2, svg, svg2, svg3, svg4; + + class App extends React.Component { + state = {step: 0}; + render() { + inst = this; + const {step} = this.state; + if (step === 0) { + return null; + } + return ( + g = el} strokeWidth="5"> + svg2 = el}> + foreignObject = el}> + svg3 = el}> + svg4 = el} /> + image = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + +
div = el} /> + + + image2 = el} xlinkHref="http://i.imgur.com/w7GCRPb.png" /> + foreignObject2 = el}> +
div2 = el} /> + + + ); + } + } + + var node = document.createElement('div'); + ReactDOM.render( + svg = el}> + + , + node + ); + inst.setState({step: 1}); + + [svg, svg2, svg3, svg4].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + // SVG tagName is case sensitive. + expect(el.tagName).toBe('svg'); + }); + expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(g.tagName).toBe('g'); + expect(g.getAttribute('stroke-width')).toBe('5'); + [image, image2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('image'); + expect( + el.getAttributeNS('http://www.w3.org/1999/xlink', 'href') + ).toBe('http://i.imgur.com/w7GCRPb.png'); + }); + [foreignObject, foreignObject2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(el.tagName).toBe('foreignObject'); + }); + [div, div2].forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + // DOM tagName is capitalized by browsers. + expect(el.tagName).toBe('DIV'); + }); + }); + }); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 1e2435aa62cd5..a712411ebe30b 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -146,6 +146,21 @@ var NoopRenderer = ReactFiberReconciler({ resetAfterCommit() : void { }, + pushHostContext() : void { + }, + + popHostContext() : void { + }, + + resetHostContext() : void { + }, + + pushHostPortal() : void { + }, + + popHostPortal() : void { + }, + }); var rootContainers = new Map(); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 812614dd34e4c..50f48728259ac 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -71,6 +71,12 @@ module.exports = function( updateClassInstance, } = ReactFiberClassComponent(scheduleUpdate); + const { + pushHostContext, + pushHostPortal, + resetHostContext, + } = config; + function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. workInProgress.progressedChild = workInProgress.child; @@ -268,6 +274,7 @@ module.exports = function( // Abort and don't process children yet. return null; } else { + pushHostContext(workInProgress.type); reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; } @@ -314,6 +321,8 @@ module.exports = function( } function updatePortalComponent(current, workInProgress) { + pushHostPortal(); + const priorityLevel = workInProgress.pendingWorkPriority; const nextChildren = workInProgress.pendingProps; if (!current) { @@ -355,8 +364,9 @@ module.exports = function( function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber { const priorityLevel = workInProgress.pendingWorkPriority; + const isHostComponent = workInProgress.tag === HostComponent; - if (workInProgress.tag === HostComponent && + if (isHostComponent && workInProgress.memoizedProps.hidden && workInProgress.pendingWorkPriority !== OffscreenPriority) { // This subtree still has work, but it should be deprioritized so we need @@ -399,8 +409,12 @@ module.exports = function( cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); // Put context on the stack because we will work on children - if (isContextProvider(workInProgress)) { + if (isHostComponent) { + pushHostContext(workInProgress.type); + } else if (isContextProvider(workInProgress)) { pushContextProvider(workInProgress, false); + } else if (workInProgress.tag === HostPortal) { + pushHostPortal(); } return workInProgress.child; } @@ -415,6 +429,7 @@ module.exports = function( if (!workInProgress.return) { // Don't start new work with context on the stack. resetContext(); + resetHostContext(); } if (workInProgress.pendingWorkPriority === NoWork || @@ -485,7 +500,6 @@ module.exports = function( return null; case HostPortal: updatePortalComponent(current, workInProgress); - // TODO: is this right? return workInProgress.child; case Fragment: updateFragment(current, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index f683a08331e71..7337d20b36c9e 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -45,11 +45,15 @@ var { module.exports = function(config : HostConfig) { - const createInstance = config.createInstance; - const appendInitialChild = config.appendInitialChild; - const finalizeInitialChildren = config.finalizeInitialChildren; - const createTextInstance = config.createTextInstance; - const prepareUpdate = config.prepareUpdate; + const { + createInstance, + appendInitialChild, + finalizeInitialChildren, + createTextInstance, + prepareUpdate, + popHostContext, + popHostPortal, + } = config; function markUpdate(workInProgress : Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -202,6 +206,7 @@ module.exports = function(config : HostConfig) { return null; } case HostComponent: + popHostContext(workInProgress.type); let newProps = workInProgress.pendingProps; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to @@ -291,6 +296,7 @@ module.exports = function(config : HostConfig) { workInProgress.memoizedProps = workInProgress.pendingProps; return null; case HostPortal: + popHostPortal(); // TODO: Only mark this as an update if we have any pending callbacks. markUpdate(workInProgress); workInProgress.memoizedProps = workInProgress.pendingProps; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 58cfbd19bb383..22ff04e928b61 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -65,6 +65,13 @@ export type HostConfig = { prepareForCommit() : void, resetAfterCommit() : void, + pushHostContext(type : T) : void, + popHostContext(type : T) : void, + resetHostContext() : void, + + pushHostPortal() : void, + popHostPortal() : void, + useSyncScheduling ?: boolean, }; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4154a365255f8..9610cd76cd2ad 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -46,7 +46,9 @@ var { } = require('ReactTypeOfSideEffect'); var { + HostComponent, HostRoot, + HostPortal, ClassComponent, } = require('ReactTypeOfWork'); @@ -74,6 +76,9 @@ module.exports = function(config : HostConfig) { const prepareForCommit = config.prepareForCommit; const resetAfterCommit = config.resetAfterCommit; + const popHostContext = config.popHostContext; + const popHostPortal = config.popHostPortal; + // The priority level to use when scheduling an update. let priorityContext : PriorityLevel = useSyncScheduling ? SynchronousPriority : @@ -583,6 +588,10 @@ module.exports = function(config : HostConfig) { // props, the nodes higher up in the tree will rerender unnecessarily. if (failedWork) { unwindContext(failedWork, boundary); + // TODO: disabling this doesn't fail any tests because we don't + // do any more host work and immediately restart from the root: + unwindHostContext(failedWork, boundary); + // This seems like a bug in error boundaries. } nextUnitOfWork = completeUnitOfWork(boundary); @@ -699,6 +708,21 @@ module.exports = function(config : HostConfig) { } } + function unwindHostContext(from : Fiber, to: Fiber) { + let node = from; + while (node && (node !== to) && (node.alternate !== to)) { + switch (node.tag) { + case HostComponent: + popHostContext(node.type); + break; + case HostPortal: + popHostPortal(); + break; + } + node = node.return; + } + } + function scheduleWork(root : FiberRoot) { let priorityLevel = priorityContext;