Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 => (
<LoggedInUser.Provider value={user}>
<header>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</header>
<footer>
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
</footer>
</LoggedInUser.Provider>
);

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('<header>Amy</header><footer>Amy</footer>');
expect(streamBob.read()).toBe('<header>Bob</header><footer>Bob</footer>');
});
});
});
65 changes: 22 additions & 43 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,9 +692,7 @@ class ReactDOMServerRenderer {
previousWasTextNode: boolean;
makeStaticMarkup: boolean;

contextIndex: number;
contextStack: Array<ReactContext<any>>;
contextValueStack: Array<any>;
contextValueStack: Array<Object>;
contextProviderStack: ?Array<ReactProvider<any>>; // DEV-only

constructor(children: mixed, makeStaticMarkup: boolean) {
Expand All @@ -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 = [];
}
Expand All @@ -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<T>(provider: ReactProvider<T>): void {
const index = ++this.contextIndex;
const context: ReactContext<any> = 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<T>(provider: ReactProvider<T>): 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<any> = 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 {
Expand Down Expand Up @@ -988,8 +961,14 @@ class ReactDOMServerRenderer {
case REACT_CONTEXT_TYPE: {
const consumer: ReactConsumer<any> = (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,
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/ReactContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
Expand All @@ -34,6 +37,10 @@ export function createContext<T>(

const context: ReactContext<T> = {
$$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
Expand Down Expand Up @@ -62,6 +69,7 @@ export function createContext<T>(
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
Expand Down
1 change: 1 addition & 0 deletions packages/shared/ReactTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type ReactContext<T> = {
Consumer: ReactContext<T>,
Provider: ReactProviderType<T>,

_uid: Symbol | string,
_calculateChangedBits: ((a: T, b: T) => number) | null,

_currentValue: T,
Expand Down