diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
index ee2afb4fae371..5f941da8f9a9f 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
@@ -348,5 +348,96 @@ describe('ReactDOMServerIntegration', () => {
await render(, 1);
},
);
+
+ it('does not pollute parallel node streams', () => {
+ const LoggedInUser = React.createContext();
+
+ const AppWithUser = user => (
+
+
+
+
+ );
+
+ 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('');
+ expect(streamBob.read()).toBe('');
+ });
+
+ it('does not pollute parallel node streams when many are used', () => {
+ const CurrentIndex = React.createContext();
+
+ const NthRender = index => (
+
+
+
+
+ );
+
+ let streams = [];
+
+ // Test with more than 32 streams to test that growing the thread count
+ // works properly.
+ let streamCount = 34;
+
+ for (let i = 0; i < streamCount; i++) {
+ streams[i] = ReactDOMServer.renderToNodeStream(
+ NthRender(i % 2 === 0 ? 'Expected to be recreated' : i),
+ ).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.
+ for (let i = 0; i < streamCount; i++) {
+ streams[i]._read(20);
+ }
+
+ // Early destroy every other stream
+ for (let i = 0; i < streamCount; i += 2) {
+ streams[i].destroy();
+ }
+
+ // Recreate those same streams.
+ for (let i = 0; i < streamCount; i += 2) {
+ streams[i] = ReactDOMServer.renderToNodeStream(
+ NthRender(i),
+ ).setEncoding('utf8');
+ }
+
+ // Read a bit from all streams again.
+ for (let i = 0; i < streamCount; i++) {
+ streams[i]._read(20);
+ }
+
+ // Assert that all stream rendered the expected output.
+ for (let i = 0; i < streamCount; i++) {
+ expect(streams[i].read()).toBe(
+ '',
+ );
+ }
+ });
});
});
diff --git a/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js b/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
index 162b8e9d76acb..7478d86cc3f18 100644
--- a/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
+++ b/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
@@ -18,11 +18,15 @@ class ReactMarkupReadableStream extends Readable {
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
}
+ _destroy() {
+ this.partialRenderer.destroy();
+ }
+
_read(size) {
try {
this.push(this.partialRenderer.read(size));
} catch (err) {
- this.emit('error', err);
+ this.destroy(err);
}
}
}
diff --git a/packages/react-dom/src/server/ReactDOMStringRenderer.js b/packages/react-dom/src/server/ReactDOMStringRenderer.js
index 50b20cb039359..1afc65acd6d5c 100644
--- a/packages/react-dom/src/server/ReactDOMStringRenderer.js
+++ b/packages/react-dom/src/server/ReactDOMStringRenderer.js
@@ -14,8 +14,12 @@ import ReactPartialRenderer from './ReactPartialRenderer';
*/
export function renderToString(element) {
const renderer = new ReactPartialRenderer(element, false);
- const markup = renderer.read(Infinity);
- return markup;
+ try {
+ const markup = renderer.read(Infinity);
+ return markup;
+ } finally {
+ renderer.destroy();
+ }
}
/**
@@ -25,6 +29,10 @@ export function renderToString(element) {
*/
export function renderToStaticMarkup(element) {
const renderer = new ReactPartialRenderer(element, true);
- const markup = renderer.read(Infinity);
- return markup;
+ try {
+ const markup = renderer.read(Infinity);
+ return markup;
+ } finally {
+ renderer.destroy();
+ }
}
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index c5f124e7c4a9a..dfc1aba8c88ce 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -7,6 +7,7 @@
* @flow
*/
+import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactElement} from 'shared/ReactElementType';
import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
@@ -16,7 +17,6 @@ import getComponentName from 'shared/getComponentName';
import lowPriorityWarning from 'shared/lowPriorityWarning';
import warning from 'shared/warning';
import warningWithoutStack from 'shared/warningWithoutStack';
-import checkPropTypes from 'prop-types/checkPropTypes';
import describeComponentFrame from 'shared/describeComponentFrame';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
@@ -39,6 +39,12 @@ import {
REACT_MEMO_TYPE,
} from 'shared/ReactSymbols';
+import {
+ emptyObject,
+ processContext,
+ validateContextBounds,
+} from './ReactPartialRendererContext';
+import {allocThreadID, freeThreadID} from './ReactThreadIDAllocator';
import {
createMarkupForCustomAttribute,
createMarkupForProperty,
@@ -50,6 +56,8 @@ import {
finishHooks,
Dispatcher,
DispatcherWithoutHooks,
+ currentThreadID,
+ setCurrentThreadID,
} from './ReactPartialRendererHooks';
import {
Namespaces,
@@ -176,7 +184,6 @@ const didWarnAboutBadClass = {};
const didWarnAboutDeprecatedWillMount = {};
const didWarnAboutUndefinedDerivedState = {};
const didWarnAboutUninitializedState = {};
-const didWarnAboutInvalidateContextType = {};
const valuePropNames = ['value', 'defaultValue'];
const newlineEatingTags = {
listing: true,
@@ -324,65 +331,6 @@ function flattenOptionChildren(children: mixed): ?string {
return content;
}
-const emptyObject = {};
-if (__DEV__) {
- Object.freeze(emptyObject);
-}
-
-function maskContext(type, context) {
- const contextTypes = type.contextTypes;
- if (!contextTypes) {
- return emptyObject;
- }
- const maskedContext = {};
- for (const contextName in contextTypes) {
- maskedContext[contextName] = context[contextName];
- }
- return maskedContext;
-}
-
-function checkContextTypes(typeSpecs, values, location: string) {
- if (__DEV__) {
- checkPropTypes(
- typeSpecs,
- values,
- location,
- 'Component',
- getCurrentServerStackImpl,
- );
- }
-}
-
-function processContext(type, context) {
- const contextType = type.contextType;
- if (typeof contextType === 'object' && contextType !== null) {
- if (__DEV__) {
- if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
- let name = getComponentName(type) || 'Component';
- if (!didWarnAboutInvalidateContextType[name]) {
- didWarnAboutInvalidateContextType[type] = true;
- warningWithoutStack(
- false,
- '%s defines an invalid contextType. ' +
- 'contextType should point to the Context object returned by React.createContext(). ' +
- 'Did you accidentally pass the Context.Provider instead?',
- name,
- );
- }
- }
- }
- return contextType._currentValue;
- } else {
- const maskedContext = maskContext(type, context);
- if (__DEV__) {
- if (type.contextTypes) {
- checkContextTypes(type.contextTypes, maskedContext, 'context');
- }
- }
- return maskedContext;
- }
-}
-
const hasOwnProperty = Object.prototype.hasOwnProperty;
const STYLE = 'style';
const RESERVED_PROPS = {
@@ -453,6 +401,7 @@ function validateRenderResult(child, type) {
function resolve(
child: mixed,
context: Object,
+ threadID: ThreadID,
): {|
child: mixed,
context: Object,
@@ -472,7 +421,7 @@ function resolve(
// Extra closure so queue and replace can be captured properly
function processChild(element, Component) {
- let publicContext = processContext(Component, context);
+ let publicContext = processContext(Component, context, threadID);
let queue = [];
let replace = false;
@@ -718,6 +667,7 @@ type FrameDev = Frame & {
};
class ReactDOMServerRenderer {
+ threadID: ThreadID;
stack: Array;
exhausted: boolean;
// TODO: type this more strictly:
@@ -747,6 +697,7 @@ class ReactDOMServerRenderer {
if (__DEV__) {
((topFrame: any): FrameDev).debugElementStack = [];
}
+ this.threadID = allocThreadID();
this.stack = [topFrame];
this.exhausted = false;
this.currentSelectValue = null;
@@ -763,6 +714,13 @@ class ReactDOMServerRenderer {
}
}
+ destroy() {
+ if (!this.exhausted) {
+ this.exhausted = true;
+ freeThreadID(this.threadID);
+ }
+ }
+
/**
* 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
@@ -776,7 +734,9 @@ class ReactDOMServerRenderer {
pushProvider(provider: ReactProvider): void {
const index = ++this.contextIndex;
const context: ReactContext = provider.type._context;
- const previousValue = context._currentValue;
+ const threadID = this.threadID;
+ validateContextBounds(context, threadID);
+ const previousValue = context[threadID];
// Remember which value to restore this context to on our way up.
this.contextStack[index] = context;
@@ -787,7 +747,7 @@ class ReactDOMServerRenderer {
}
// Mutate the current value.
- context._currentValue = provider.props.value;
+ context[threadID] = provider.props.value;
}
popProvider(provider: ReactProvider): void {
@@ -813,7 +773,9 @@ class ReactDOMServerRenderer {
this.contextIndex--;
// Restore to the previous value we stored as we were walking down.
- context._currentValue = previousValue;
+ // We've already verified that this context has been expanded to accommodate
+ // this thread id, so we don't need to do it again.
+ context[this.threadID] = previousValue;
}
read(bytes: number): string | null {
@@ -821,6 +783,8 @@ class ReactDOMServerRenderer {
return null;
}
+ const prevThreadID = currentThreadID;
+ setCurrentThreadID(this.threadID);
const prevDispatcher = ReactCurrentOwner.currentDispatcher;
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
@@ -835,6 +799,7 @@ class ReactDOMServerRenderer {
while (out[0].length < bytes) {
if (this.stack.length === 0) {
this.exhausted = true;
+ freeThreadID(this.threadID);
break;
}
const frame: Frame = this.stack[this.stack.length - 1];
@@ -906,6 +871,7 @@ class ReactDOMServerRenderer {
return out[0];
} finally {
ReactCurrentOwner.currentDispatcher = prevDispatcher;
+ setCurrentThreadID(prevThreadID);
}
}
@@ -929,7 +895,7 @@ class ReactDOMServerRenderer {
return escapeTextForBrowser(text);
} else {
let nextChild;
- ({child: nextChild, context} = resolve(child, context));
+ ({child: nextChild, context} = resolve(child, context, this.threadID));
if (nextChild === null || nextChild === false) {
return '';
} else if (!React.isValidElement(nextChild)) {
@@ -1136,7 +1102,9 @@ class ReactDOMServerRenderer {
}
}
const nextProps: any = (nextChild: any).props;
- const nextValue = reactContext._currentValue;
+ const threadID = this.threadID;
+ validateContextBounds(reactContext, threadID);
+ const nextValue = reactContext[threadID];
const nextChildren = toArray(nextProps.children(nextValue));
const frame: Frame = {
diff --git a/packages/react-dom/src/server/ReactPartialRendererContext.js b/packages/react-dom/src/server/ReactPartialRendererContext.js
new file mode 100644
index 0000000000000..6a0e1fad16f38
--- /dev/null
+++ b/packages/react-dom/src/server/ReactPartialRendererContext.js
@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {ThreadID} from './ReactThreadIDAllocator';
+import type {ReactContext} from 'shared/ReactTypes';
+
+import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
+import ReactSharedInternals from 'shared/ReactSharedInternals';
+import getComponentName from 'shared/getComponentName';
+import warningWithoutStack from 'shared/warningWithoutStack';
+import checkPropTypes from 'prop-types/checkPropTypes';
+
+let ReactDebugCurrentFrame;
+if (__DEV__) {
+ ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
+}
+
+const didWarnAboutInvalidateContextType = {};
+
+export const emptyObject = {};
+if (__DEV__) {
+ Object.freeze(emptyObject);
+}
+
+function maskContext(type, context) {
+ const contextTypes = type.contextTypes;
+ if (!contextTypes) {
+ return emptyObject;
+ }
+ const maskedContext = {};
+ for (const contextName in contextTypes) {
+ maskedContext[contextName] = context[contextName];
+ }
+ return maskedContext;
+}
+
+function checkContextTypes(typeSpecs, values, location: string) {
+ if (__DEV__) {
+ checkPropTypes(
+ typeSpecs,
+ values,
+ location,
+ 'Component',
+ ReactDebugCurrentFrame.getCurrentStack,
+ );
+ }
+}
+
+export function validateContextBounds(
+ context: ReactContext,
+ threadID: ThreadID,
+) {
+ // If we don't have enough slots in this context to store this threadID,
+ // fill it in without leaving any holes to ensure that the VM optimizes
+ // this as non-holey index properties.
+ for (let i = context._threadCount; i <= threadID; i++) {
+ // We assume that this is the same as the defaultValue which might not be
+ // true if we're rendering inside a secondary renderer but they are
+ // secondary because these use cases are very rare.
+ context[i] = context._currentValue2;
+ context._threadCount = i + 1;
+ }
+}
+
+export function processContext(
+ type: Function,
+ context: Object,
+ threadID: ThreadID,
+) {
+ const contextType = type.contextType;
+ if (typeof contextType === 'object' && contextType !== null) {
+ if (__DEV__) {
+ if (contextType.$$typeof !== REACT_CONTEXT_TYPE) {
+ let name = getComponentName(type) || 'Component';
+ if (!didWarnAboutInvalidateContextType[name]) {
+ didWarnAboutInvalidateContextType[name] = true;
+ warningWithoutStack(
+ false,
+ '%s defines an invalid contextType. ' +
+ 'contextType should point to the Context object returned by React.createContext(). ' +
+ 'Did you accidentally pass the Context.Provider instead?',
+ name,
+ );
+ }
+ }
+ }
+ validateContextBounds(contextType, threadID);
+ return contextType[threadID];
+ } else {
+ const maskedContext = maskContext(type, context);
+ if (__DEV__) {
+ if (type.contextTypes) {
+ checkContextTypes(type.contextTypes, maskedContext, 'context');
+ }
+ }
+ return maskedContext;
+ }
+}
diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js
index d3ddf0d6b6157..2860ee9bfbeb9 100644
--- a/packages/react-dom/src/server/ReactPartialRendererHooks.js
+++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js
@@ -6,9 +6,13 @@
*
* @flow
*/
+
+import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactContext} from 'shared/ReactTypes';
import areHookInputsEqual from 'shared/areHookInputsEqual';
+import {validateContextBounds} from './ReactPartialRendererContext';
+
import invariant from 'shared/invariant';
import warning from 'shared/warning';
@@ -139,7 +143,9 @@ function readContext(
context: ReactContext,
observedBits: void | number | boolean,
): T {
- return context._currentValue;
+ let threadID = currentThreadID;
+ validateContextBounds(context, threadID);
+ return context[threadID];
}
function useContext(
@@ -147,7 +153,9 @@ function useContext(
observedBits: void | number | boolean,
): T {
resolveCurrentlyRenderingComponent();
- return context._currentValue;
+ let threadID = currentThreadID;
+ validateContextBounds(context, threadID);
+ return context[threadID];
}
function basicStateReducer(state: S, action: BasicStateAction): S {
@@ -334,6 +342,12 @@ function dispatchAction(
function noop(): void {}
+export let currentThreadID: ThreadID = 0;
+
+export function setCurrentThreadID(threadID: ThreadID) {
+ currentThreadID = threadID;
+}
+
export const Dispatcher = {
readContext,
useContext,
diff --git a/packages/react-dom/src/server/ReactThreadIDAllocator.js b/packages/react-dom/src/server/ReactThreadIDAllocator.js
new file mode 100644
index 0000000000000..1bba2dd50fcac
--- /dev/null
+++ b/packages/react-dom/src/server/ReactThreadIDAllocator.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// Allocates a new index for each request. Tries to stay as compact as possible so that these
+// indices can be used to reference a tightly packaged array. As opposed to being used in a Map.
+// The first allocated index is 1.
+
+import invariant from 'shared/invariant';
+
+export type ThreadID = number;
+
+let nextAvailableThreadIDs = new Uint16Array(16);
+for (let i = 0; i < 15; i++) {
+ nextAvailableThreadIDs[i] = i + 1;
+}
+nextAvailableThreadIDs[15] = 0;
+
+function growThreadCountAndReturnNextAvailable() {
+ let oldArray = nextAvailableThreadIDs;
+ let oldSize = oldArray.length;
+ let newSize = oldSize * 2;
+ invariant(
+ newSize <= 0x10000,
+ 'Maximum number of concurrent React renderers exceeded. ' +
+ 'This can happen if you are not properly destroying the Readable provided by React. ' +
+ 'Ensure that you call .destroy() on it if you no longer want to read from it.' +
+ ', and did not read to the end. If you use .pipe() this should be automatic.',
+ );
+ let newArray = new Uint16Array(newSize);
+ newArray.set(oldArray);
+ nextAvailableThreadIDs = newArray;
+ nextAvailableThreadIDs[0] = oldSize + 1;
+ for (let i = oldSize; i < newSize - 1; i++) {
+ nextAvailableThreadIDs[i] = i + 1;
+ }
+ nextAvailableThreadIDs[newSize - 1] = 0;
+ return oldSize;
+}
+
+export function allocThreadID(): ThreadID {
+ let nextID = nextAvailableThreadIDs[0];
+ if (nextID === 0) {
+ return growThreadCountAndReturnNextAvailable();
+ }
+ nextAvailableThreadIDs[0] = nextAvailableThreadIDs[nextID];
+ return nextID;
+}
+
+export function freeThreadID(id: ThreadID) {
+ nextAvailableThreadIDs[id] = nextAvailableThreadIDs[0];
+ nextAvailableThreadIDs[0] = id;
+}
diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js
index 71d1b38605446..643a13019e074 100644
--- a/packages/react/src/ReactContext.js
+++ b/packages/react/src/ReactContext.js
@@ -42,6 +42,9 @@ export function createContext(
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
+ // Used to track how many concurrent renderers this context currently
+ // supports within in a single renderer. Such as parallel server rendering.
+ _threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
@@ -98,6 +101,14 @@ export function createContext(
context._currentValue2 = _currentValue2;
},
},
+ _threadCount: {
+ get() {
+ return context._threadCount;
+ },
+ set(_threadCount) {
+ context._threadCount = _threadCount;
+ },
+ },
Consumer: {
get() {
if (!hasWarnedAboutUsingNestedContextConsumers) {
diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js
index 080ade12b807b..f09207928ceaf 100644
--- a/packages/shared/ReactTypes.js
+++ b/packages/shared/ReactTypes.js
@@ -59,6 +59,7 @@ export type ReactContext = {
_currentValue: T,
_currentValue2: T,
+ _threadCount: number,
// DEV only
_currentRenderer?: Object | null,
diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js
index 82c236f7c2228..a1b4a7fdfac44 100644
--- a/scripts/rollup/validate/eslintrc.cjs.js
+++ b/scripts/rollup/validate/eslintrc.cjs.js
@@ -12,6 +12,7 @@ module.exports = {
Proxy: true,
Symbol: true,
WeakMap: true,
+ Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,
diff --git a/scripts/rollup/validate/eslintrc.fb.js b/scripts/rollup/validate/eslintrc.fb.js
index 527fd0a473c98..2e2949444f5da 100644
--- a/scripts/rollup/validate/eslintrc.fb.js
+++ b/scripts/rollup/validate/eslintrc.fb.js
@@ -12,6 +12,7 @@ module.exports = {
Symbol: true,
Proxy: true,
WeakMap: true,
+ Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,
diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js
index 7ee1e112f2b09..a3d5fadf2c55e 100644
--- a/scripts/rollup/validate/eslintrc.umd.js
+++ b/scripts/rollup/validate/eslintrc.umd.js
@@ -11,6 +11,7 @@ module.exports = {
Symbol: true,
Proxy: true,
WeakMap: true,
+ Uint16Array: true,
// Vendor specific
MSApp: true,
__REACT_DEVTOOLS_GLOBAL_HOOK__: true,