Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ab7a67b

Browse files
overlookmotelgaearon
overlookmotel
authored andcommitted
Fix react-dom/server context leaks when render stream destroyed early (facebook#14706)
* Fix react-dom/server context memory retention * Test for pollution of later renders * Inline loop * More tests
1 parent 3e55560 commit ab7a67b

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,5 +482,98 @@ describe('ReactDOMServerIntegration', () => {
482482
);
483483
}
484484
});
485+
486+
// Regression test for https://github.com/facebook/react/issues/14705
487+
it('does not pollute later renders when stream destroyed', () => {
488+
const LoggedInUser = React.createContext('default');
489+
490+
const AppWithUser = user => (
491+
<LoggedInUser.Provider value={user}>
492+
<header>
493+
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
494+
</header>
495+
</LoggedInUser.Provider>
496+
);
497+
498+
const stream = ReactDOMServer.renderToNodeStream(
499+
AppWithUser('Amy'),
500+
).setEncoding('utf8');
501+
502+
// This is an implementation detail because we test a memory leak
503+
const {threadID} = stream.partialRenderer;
504+
505+
// Read enough to render Provider but not enough for it to be exited
506+
stream._read(10);
507+
expect(LoggedInUser[threadID]).toBe('Amy');
508+
509+
stream.destroy();
510+
511+
const AppWithUserNoProvider = () => (
512+
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
513+
);
514+
515+
const stream2 = ReactDOMServer.renderToNodeStream(
516+
AppWithUserNoProvider(),
517+
).setEncoding('utf8');
518+
519+
// Sanity check to ensure 2nd render has same threadID as 1st render,
520+
// otherwise this test is not testing what it's meant to
521+
expect(stream2.partialRenderer.threadID).toBe(threadID);
522+
523+
const markup = stream2.read(Infinity);
524+
525+
expect(markup).toBe('default');
526+
});
527+
528+
// Regression test for https://github.com/facebook/react/issues/14705
529+
it('frees context value reference when stream destroyed', () => {
530+
const LoggedInUser = React.createContext('default');
531+
532+
const AppWithUser = user => (
533+
<LoggedInUser.Provider value={user}>
534+
<header>
535+
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
536+
</header>
537+
</LoggedInUser.Provider>
538+
);
539+
540+
const stream = ReactDOMServer.renderToNodeStream(
541+
AppWithUser('Amy'),
542+
).setEncoding('utf8');
543+
544+
// This is an implementation detail because we test a memory leak
545+
const {threadID} = stream.partialRenderer;
546+
547+
// Read enough to render Provider but not enough for it to be exited
548+
stream._read(10);
549+
expect(LoggedInUser[threadID]).toBe('Amy');
550+
551+
stream.destroy();
552+
expect(LoggedInUser[threadID]).toBe('default');
553+
});
554+
555+
it('does not pollute sync renders after an error', () => {
556+
const LoggedInUser = React.createContext('default');
557+
const Crash = () => {
558+
throw new Error('Boo!');
559+
};
560+
const AppWithUser = user => (
561+
<LoggedInUser.Provider value={user}>
562+
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>
563+
<Crash />
564+
</LoggedInUser.Provider>
565+
);
566+
567+
expect(() => {
568+
ReactDOMServer.renderToString(AppWithUser('Casper'));
569+
}).toThrow('Boo');
570+
571+
// Should not report a value from failed render
572+
expect(
573+
ReactDOMServer.renderToString(
574+
<LoggedInUser.Consumer>{whoAmI => whoAmI}</LoggedInUser.Consumer>,
575+
),
576+
).toBe('default');
577+
});
485578
});
486579
});

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ class ReactDOMServerRenderer {
715715
destroy() {
716716
if (!this.exhausted) {
717717
this.exhausted = true;
718+
this.clearProviders();
718719
freeThreadID(this.threadID);
719720
}
720721
}
@@ -776,6 +777,15 @@ class ReactDOMServerRenderer {
776777
context[this.threadID] = previousValue;
777778
}
778779

780+
clearProviders(): void {
781+
// Restore any remaining providers on the stack to previous values
782+
for (let index = this.contextIndex; index >= 0; index--) {
783+
const context: ReactContext<any> = this.contextStack[index];
784+
const previousValue = this.contextValueStack[index];
785+
context[this.threadID] = previousValue;
786+
}
787+
}
788+
779789
read(bytes: number): string | null {
780790
if (this.exhausted) {
781791
return null;

0 commit comments

Comments
 (0)