diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index f33ffd239eb2a..f04f06e4b9194 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -641,8 +641,10 @@ class ReactDOMServerRenderer { previousWasTextNode: boolean; makeStaticMarkup: boolean; - providerStack: Array>; - providerIndex: number; + contextIndex: number; + contextStack: Array>; + contextValueStack: Array; + contextProviderStack: ?Array>; // DEV-only constructor(children: mixed, makeStaticMarkup: boolean) { const flatChildren = flattenTopLevelChildren(children); @@ -667,46 +669,66 @@ class ReactDOMServerRenderer { this.makeStaticMarkup = makeStaticMarkup; // Context (new API) - this.providerStack = []; // Stack of provider objects - this.providerIndex = -1; + this.contextIndex = -1; + this.contextStack = []; + this.contextValueStack = []; + if (__DEV__) { + this.contextProviderStack = []; + } } + /** + * Note: We use just two stacks regardless of how many context providers you have. + * Providers are always popped in the reverse order to how they were pushed + * so we always know on the way down which provider you'll encounter next on the way up. + * On the way down, we push the current provider, and its context value *before* + * we mutated it, onto the stacks. Therefore, on the way up, we always know which + * provider needs to be "restored" to which value. + * https://github.com/facebook/react/pull/12985#issuecomment-396301248 + */ + pushProvider(provider: ReactProvider): void { - this.providerIndex += 1; - this.providerStack[this.providerIndex] = provider; + const index = ++this.contextIndex; const context: ReactContext = provider.type._context; + const previousValue = context._currentValue; + + // Remember which value to restore this context to on our way up. + this.contextStack[index] = context; + this.contextValueStack[index] = previousValue; + if (__DEV__) { + // Only used for push/pop mismatch warnings. + (this.contextProviderStack: any)[index] = provider; + } + + // Mutate the current value. context._currentValue = provider.props.value; } popProvider(provider: ReactProvider): void { + const index = this.contextIndex; if (__DEV__) { warning( - this.providerIndex > -1 && - provider === this.providerStack[this.providerIndex], + index > -1 && provider === (this.contextProviderStack: any)[index], 'Unexpected pop.', ); } - this.providerStack[this.providerIndex] = null; - this.providerIndex -= 1; - const context: ReactContext = provider.type._context; - // Find the closest parent provider of the same type and use its value. - // TODO: it would be nice to avoid this being O(N). - let contextPriorProvider = null; - for (let i = this.providerIndex; i >= 0; i--) { - // We assume this Flow type is correct because of the index check above - // and because pushProvider() enforces the correct type. - const priorProvider: ReactProvider = (this.providerStack[i]: any); - if (priorProvider.type === provider.type) { - contextPriorProvider = priorProvider; - break; - } - } - if (contextPriorProvider !== null) { - context._currentValue = contextPriorProvider.props.value; - } else { - context._currentValue = context._defaultValue; + // We assume it's not null because there is a push for every pop. + const context: ReactContext = (this.contextStack[index]: any); + const previousValue = this.contextValueStack[index]; + + // "Hide" these null assignments from Flow by using `any` + // because conceptually they are deletions--as long as we + // promise to never access values beyond `this.contextIndex`. + this.contextStack[index] = (null: any); + this.contextValueStack[index] = (null: any); + if (__DEV__) { + (this.contextProviderStack: any)[index] = (null: any); } + + // Restore to the previous value we stored as we were walking down. + this.contextIndex--; + context._currentValue = previousValue; } read(bytes: number): string | null {