diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js index af9a6095e0a4..334ff0e2cbd2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js @@ -249,5 +249,38 @@ describe('ReactDOMServerIntegration', () => { expect(e.querySelector('#language2').textContent).toBe('sanskrit'); expect(e.querySelector('#language3').textContent).toBe('french'); }); + + it('does not pollute parallel node streams', () => { + const LoggedInUser = React.createContext(); + + const AppWithUser = user => ( + +
+ {whoAmI => whoAmI} +
+ +
+ ); + + const streamAmy = ReactDOMServer.renderToNodeStream( + AppWithUser('Amy'), + ).setEncoding('utf8'); + const streamBob = ReactDOMServer.renderToNodeStream( + AppWithUser('Bob'), + ).setEncoding('utf8'); + + // Testing by filling the buffer using internal _read() with a small + // number of bytes to avoid a test case which needs to align to a + // highWaterMark boundary of 2^14 chars. + streamAmy._read(20); + streamBob._read(20); + streamAmy._read(20); + streamBob._read(20); + + expect(streamAmy.read()).toBe('
Amy
'); + expect(streamBob.read()).toBe('
Bob
'); + }); }); }); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index dc79c3fb2803..c0277ff6f263 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -692,9 +692,7 @@ class ReactDOMServerRenderer { previousWasTextNode: boolean; makeStaticMarkup: boolean; - contextIndex: number; - contextStack: Array>; - contextValueStack: Array; + contextValueStack: Array; contextProviderStack: ?Array>; // DEV-only constructor(children: mixed, makeStaticMarkup: boolean) { @@ -720,9 +718,7 @@ class ReactDOMServerRenderer { this.makeStaticMarkup = makeStaticMarkup; // Context (new API) - this.contextIndex = -1; - this.contextStack = []; - this.contextValueStack = []; + this.contextValueStack = [emptyObject]; if (__DEV__) { this.contextProviderStack = []; } @@ -732,53 +728,30 @@ class ReactDOMServerRenderer { * 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 + * On the way down, we push the current provider, and its context value onto + * the stacks, without mutating. */ pushProvider(provider: ReactProvider): void { - 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; + const prevValues = this.contextValueStack[ + this.contextValueStack.length - 1 + ]; + const nextValues = Object.assign({}, prevValues); + nextValues[context._uid] = provider.props.value; + this.contextValueStack.push(nextValues); if (__DEV__) { // Only used for push/pop mismatch warnings. - (this.contextProviderStack: any)[index] = provider; + (this.contextProviderStack: any).push(provider); } - - // Mutate the current value. - context._currentValue = provider.props.value; } popProvider(provider: ReactProvider): void { - const index = this.contextIndex; + this.contextValueStack.pop(); if (__DEV__) { - warningWithoutStack( - index > -1 && provider === (this.contextProviderStack: any)[index], - 'Unexpected pop.', - ); + const poppedProvider = (this.contextProviderStack: any).pop(); + warningWithoutStack(provider === poppedProvider, 'Unexpected pop.'); } - - const context: ReactContext = this.contextStack[index]; - 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); - } - this.contextIndex--; - - // Restore to the previous value we stored as we were walking down. - context._currentValue = previousValue; } read(bytes: number): string | null { @@ -988,8 +961,14 @@ class ReactDOMServerRenderer { case REACT_CONTEXT_TYPE: { const consumer: ReactConsumer = (nextChild: any); const nextProps: any = consumer.props; - const nextValue = consumer.type._currentValue; - + const values = this.contextValueStack[ + this.contextValueStack.length - 1 + ]; + // Use the value provided, or if not defined, fall back to the + // default value for this context. + const nextValue = values.hasOwnProperty(consumer.type._uid) + ? values[consumer.type._uid] + : consumer.type._currentValue; const nextChildren = toArray(nextProps.children(nextValue)); const frame: Frame = { type: nextChild, diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 71d1b3860544..836b95a64691 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -14,6 +14,9 @@ import type {ReactContext} from 'shared/ReactTypes'; import warningWithoutStack from 'shared/warningWithoutStack'; import warning from 'shared/warning'; +const hasSymbol = typeof Symbol === 'function'; +let nextContextID = 1; + export function createContext( defaultValue: T, calculateChangedBits: ?(a: T, b: T) => number, @@ -34,6 +37,10 @@ export function createContext( const context: ReactContext = { $$typeof: REACT_CONTEXT_TYPE, + // A unique key to use when storing and retrieving values for this context + // instance. Uses an opaque Symbol when available, otherwise uses an + // incrementing numeric string. + _uid: hasSymbol ? Symbol() : '' + nextContextID++, _calculateChangedBits: calculateChangedBits, // As a workaround to support multiple concurrent renderers, we categorize // some renderers as primary and others as secondary. We only expect @@ -62,6 +69,7 @@ export function createContext( const Consumer = { $$typeof: REACT_CONTEXT_TYPE, _context: context, + _uid: context._uid, _calculateChangedBits: context._calculateChangedBits, }; // $FlowFixMe: Flow complains about not setting a value, which is intentional here diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 080ade12b807..99a7c34f6e59 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -55,6 +55,7 @@ export type ReactContext = { Consumer: ReactContext, Provider: ReactProviderType, + _uid: Symbol | string, _calculateChangedBits: ((a: T, b: T) => number) | null, _currentValue: T,