diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js index af9a6095e0a46..334ff0e2cbd2c 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 dc79c3fb28031..68342ceecad24 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -695,6 +695,7 @@ class ReactDOMServerRenderer { contextIndex: number; contextStack: Array>; contextValueStack: Array; + contextValueMap: Map, any>; contextProviderStack: ?Array>; // DEV-only constructor(children: mixed, makeStaticMarkup: boolean) { @@ -723,6 +724,7 @@ class ReactDOMServerRenderer { this.contextIndex = -1; this.contextStack = []; this.contextValueStack = []; + this.contextValueMap = new Map(); if (__DEV__) { this.contextProviderStack = []; } @@ -736,12 +738,21 @@ class ReactDOMServerRenderer { * 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 + * + * A context's _currentValue is not directly mutated to avoid issues with + * concurrent renderers. Instead a local contextValueMap stores the current + * value for each context instance. */ pushProvider(provider: ReactProvider): void { const index = ++this.contextIndex; - const context: ReactContext = provider.type._context; - const previousValue = context._currentValue; + // NOTE: in __DEV__ the _context and Consumer references are different, + // while in production they are the same. For consistent Map keys, always + // use context.Consumer. + const context: ReactContext = provider.type._context.Consumer; + const previousValue = this.contextValueMap.has(context) + ? this.contextValueMap.get(context) + : context._currentValue; // Remember which value to restore this context to on our way up. this.contextStack[index] = context; @@ -752,7 +763,7 @@ class ReactDOMServerRenderer { } // Mutate the current value. - context._currentValue = provider.props.value; + this.contextValueMap.set(context, provider.props.value); } popProvider(provider: ReactProvider): void { @@ -778,7 +789,7 @@ class ReactDOMServerRenderer { this.contextIndex--; // Restore to the previous value we stored as we were walking down. - context._currentValue = previousValue; + this.contextValueMap.set(context, previousValue); } read(bytes: number): string | null { @@ -988,7 +999,9 @@ class ReactDOMServerRenderer { case REACT_CONTEXT_TYPE: { const consumer: ReactConsumer = (nextChild: any); const nextProps: any = consumer.props; - const nextValue = consumer.type._currentValue; + const nextValue = this.contextValueMap.has(consumer.type) + ? this.contextValueMap.get(consumer.type) + : consumer.type._currentValue; const nextChildren = toArray(nextProps.children(nextValue)); const frame: Frame = {