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 => ( + +
+ {whoAmI => whoAmI} +
+
+ ); + + 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 => ( + +
+ {whoAmI => whoAmI} +
+
+ ); + + 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;