diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
index 1060cb244baba..071dd8715751c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js
@@ -482,5 +482,98 @@ describe('ReactDOMServerIntegration', () => {
);
}
});
+
+ // Regression test for https://github.com/facebook/react/issues/14705
+ it('does not pollute later renders when stream destroyed', () => {
+ const LoggedInUser = React.createContext('default');
+
+ const AppWithUser = user => (
+
+
+
+ );
+
+ const stream = ReactDOMServer.renderToNodeStream(
+ AppWithUser('Amy'),
+ ).setEncoding('utf8');
+
+ // This is an implementation detail because we test a memory leak
+ const {threadID} = stream.partialRenderer;
+
+ // Read enough to render Provider but not enough for it to be exited
+ stream._read(10);
+ expect(LoggedInUser[threadID]).toBe('Amy');
+
+ stream.destroy();
+
+ const AppWithUserNoProvider = () => (
+ {whoAmI => whoAmI}
+ );
+
+ const stream2 = ReactDOMServer.renderToNodeStream(
+ AppWithUserNoProvider(),
+ ).setEncoding('utf8');
+
+ // Sanity check to ensure 2nd render has same threadID as 1st render,
+ // otherwise this test is not testing what it's meant to
+ expect(stream2.partialRenderer.threadID).toBe(threadID);
+
+ const markup = stream2.read(Infinity);
+
+ expect(markup).toBe('default');
+ });
+
+ // Regression test for https://github.com/facebook/react/issues/14705
+ it('frees context value reference when stream destroyed', () => {
+ const LoggedInUser = React.createContext('default');
+
+ const AppWithUser = user => (
+
+
+
+ );
+
+ const stream = ReactDOMServer.renderToNodeStream(
+ AppWithUser('Amy'),
+ ).setEncoding('utf8');
+
+ // This is an implementation detail because we test a memory leak
+ const {threadID} = stream.partialRenderer;
+
+ // Read enough to render Provider but not enough for it to be exited
+ stream._read(10);
+ expect(LoggedInUser[threadID]).toBe('Amy');
+
+ stream.destroy();
+ expect(LoggedInUser[threadID]).toBe('default');
+ });
+
+ it('does not pollute sync renders after an error', () => {
+ const LoggedInUser = React.createContext('default');
+ const Crash = () => {
+ throw new Error('Boo!');
+ };
+ const AppWithUser = user => (
+
+ {whoAmI => whoAmI}
+
+
+ );
+
+ expect(() => {
+ ReactDOMServer.renderToString(AppWithUser('Casper'));
+ }).toThrow('Boo');
+
+ // Should not report a value from failed render
+ expect(
+ ReactDOMServer.renderToString(
+ {whoAmI => whoAmI},
+ ),
+ ).toBe('default');
+ });
});
});
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index 1d9a7157b21cb..120403e89dcc8 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -715,6 +715,7 @@ class ReactDOMServerRenderer {
destroy() {
if (!this.exhausted) {
this.exhausted = true;
+ this.clearProviders();
freeThreadID(this.threadID);
}
}
@@ -776,6 +777,15 @@ class ReactDOMServerRenderer {
context[this.threadID] = previousValue;
}
+ clearProviders(): void {
+ // Restore any remaining providers on the stack to previous values
+ for (let index = this.contextIndex; index >= 0; index--) {
+ const context: ReactContext = this.contextStack[index];
+ const previousValue = this.contextValueStack[index];
+ context[this.threadID] = previousValue;
+ }
+ }
+
read(bytes: number): string | null {
if (this.exhausted) {
return null;
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index 0b5f0172e7ea1..830e87ccc6612 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -607,7 +607,6 @@ function updateReducer(
}
hook.memoizedState = newState;
-
// Don't persist the state accumlated from the render phase updates to
// the base state unless the queue is empty.
// TODO: Not sure if this is the desired semantics, but it's what we
@@ -616,6 +615,9 @@ function updateReducer(
hook.baseState = newState;
}
+ queue.eagerReducer = reducer;
+ queue.eagerState = newState;
+
return [newState, dispatch];
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
index f7ab26011b64e..2e11f01d5a2d6 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
@@ -669,6 +669,76 @@ describe('ReactHooks', () => {
}).toThrow('is not a function');
});
+ it('does not forget render phase useState updates inside an effect', () => {
+ const {useState, useEffect} = React;
+
+ function Counter() {
+ const [counter, setCounter] = useState(0);
+ if (counter === 0) {
+ setCounter(x => x + 1);
+ setCounter(x => x + 1);
+ }
+ useEffect(() => {
+ setCounter(x => x + 1);
+ setCounter(x => x + 1);
+ }, []);
+ return counter;
+ }
+
+ const root = ReactTestRenderer.create(null);
+ ReactTestRenderer.act(() => {
+ root.update();
+ });
+ expect(root).toMatchRenderedOutput('4');
+ });
+
+ it('does not forget render phase useReducer updates inside an effect with hoisted reducer', () => {
+ const {useReducer, useEffect} = React;
+
+ const reducer = x => x + 1;
+ function Counter() {
+ const [counter, increment] = useReducer(reducer, 0);
+ if (counter === 0) {
+ increment();
+ increment();
+ }
+ useEffect(() => {
+ increment();
+ increment();
+ }, []);
+ return counter;
+ }
+
+ const root = ReactTestRenderer.create(null);
+ ReactTestRenderer.act(() => {
+ root.update();
+ });
+ expect(root).toMatchRenderedOutput('4');
+ });
+
+ it('does not forget render phase useReducer updates inside an effect with inline reducer', () => {
+ const {useReducer, useEffect} = React;
+
+ function Counter() {
+ const [counter, increment] = useReducer(x => x + 1, 0);
+ if (counter === 0) {
+ increment();
+ increment();
+ }
+ useEffect(() => {
+ increment();
+ increment();
+ }, []);
+ return counter;
+ }
+
+ const root = ReactTestRenderer.create(null);
+ ReactTestRenderer.act(() => {
+ root.update();
+ });
+ expect(root).toMatchRenderedOutput('4');
+ });
+
it('warns for bad useImperativeHandle first arg', () => {
const {useImperativeHandle} = React;
function App() {
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
index 290651b117e77..2995a3349759c 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
@@ -454,7 +454,9 @@ describe('ReactHooksWithNoopRenderer', () => {
// Test that it works on update, too. This time the log is a bit different
// because we started with reducerB instead of reducerA.
- counter.current.dispatch('reset');
+ ReactNoop.act(() => {
+ counter.current.dispatch('reset');
+ });
ReactNoop.render();
expect(ReactNoop.flush()).toEqual([
'Render: 0',
diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js
index 1fe0c2391bd13..b4d1b9358c483 100644
--- a/packages/react/src/ReactSharedInternals.js
+++ b/packages/react/src/ReactSharedInternals.js
@@ -18,6 +18,11 @@ import {
unstable_continueExecution,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
+ unstable_IdlePriority,
+ unstable_ImmediatePriority,
+ unstable_LowPriority,
+ unstable_NormalPriority,
+ unstable_UserBlockingPriority,
} from 'scheduler';
import {
__interactionsRef,
@@ -60,6 +65,11 @@ if (__UMD__) {
unstable_pauseExecution,
unstable_continueExecution,
unstable_getCurrentPriorityLevel,
+ unstable_IdlePriority,
+ unstable_ImmediatePriority,
+ unstable_LowPriority,
+ unstable_NormalPriority,
+ unstable_UserBlockingPriority,
},
SchedulerTracing: {
__interactionsRef,
diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js
index ac632eb288bff..206897dcfb97f 100644
--- a/packages/scheduler/npm/umd/scheduler.development.js
+++ b/packages/scheduler/npm/umd/scheduler.development.js
@@ -108,5 +108,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
+ get unstable_IdlePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_IdlePriority;
+ },
+ get unstable_ImmediatePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_ImmediatePriority;
+ },
+ get unstable_LowPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_LowPriority;
+ },
+ get unstable_NormalPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_NormalPriority;
+ },
+ get unstable_UserBlockingPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_UserBlockingPriority;
+ },
});
});
diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js
index da2aefa9e4bf1..90ee11f122d92 100644
--- a/packages/scheduler/npm/umd/scheduler.production.min.js
+++ b/packages/scheduler/npm/umd/scheduler.production.min.js
@@ -102,5 +102,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
+ get unstable_IdlePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_IdlePriority;
+ },
+ get unstable_ImmediatePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_ImmediatePriority;
+ },
+ get unstable_LowPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_LowPriority;
+ },
+ get unstable_NormalPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_NormalPriority;
+ },
+ get unstable_UserBlockingPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_UserBlockingPriority;
+ },
});
});
diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js
index da2aefa9e4bf1..90ee11f122d92 100644
--- a/packages/scheduler/npm/umd/scheduler.profiling.min.js
+++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js
@@ -102,5 +102,25 @@
unstable_continueExecution: unstable_continueExecution,
unstable_pauseExecution: unstable_pauseExecution,
unstable_getFirstCallbackNode: unstable_getFirstCallbackNode,
+ get unstable_IdlePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_IdlePriority;
+ },
+ get unstable_ImmediatePriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_ImmediatePriority;
+ },
+ get unstable_LowPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_LowPriority;
+ },
+ get unstable_NormalPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_NormalPriority;
+ },
+ get unstable_UserBlockingPriority() {
+ return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
+ .Scheduler.unstable_UserBlockingPriority;
+ },
});
});
diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
index 8fd78522480f1..b22d2c27704ce 100644
--- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
+++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js
@@ -17,8 +17,17 @@ describe('Scheduling UMD bundle', () => {
});
function filterPrivateKeys(name) {
- // TODO: Figure out how to forward priority levels.
- return !name.startsWith('_') && !name.endsWith('Priority');
+ // Be very careful adding things to this whitelist!
+ // It's easy to introduce bugs by doing it:
+ // https://github.com/facebook/react/issues/14904
+ switch (name) {
+ case '__interactionsRef':
+ case '__subscriberRef':
+ // Don't forward these. (TODO: why?)
+ return false;
+ default:
+ return true;
+ }
}
function validateForwardedAPIs(api, forwardedAPIs) {