From 1e2a0d4b11de918e33116af176daadc9ce77994e Mon Sep 17 00:00:00 2001 From: Fran Dios Date: Tue, 25 Jan 2022 02:52:51 +0900 Subject: [PATCH] Fix context providers in SSR when handling multiple requests (#23171) * add failing test for renderToPipeableStream * Fix context providers in SSR when handling multiple requests. Closes #23089 * Add sibling regression test Co-authored-by: zhuyi01 Co-authored-by: Dan Abramov --- .../__tests__/ReactDOMFizzServerNode-test.js | 184 ++++++++++++++++++ .../react-server/src/ReactFizzNewContext.js | 5 +- 2 files changed, 187 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index bd0ca112a272b..a453e0347d8d5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -338,4 +338,188 @@ describe('ReactDOMFizzServer', () => { expect(output.result).toContain('Loading'); expect(isCompleteCalls).toBe(1); }); + + // @gate experimental + it('should be able to get context value when promise resolves', async () => { + class DelayClient { + get() { + if (this.resolved) return this.resolved; + if (this.pending) return this.pending; + return (this.pending = new Promise(resolve => { + setTimeout(() => { + delete this.pending; + this.resolved = 'OK'; + resolve(); + }, 500); + })); + } + } + + const DelayContext = React.createContext(undefined); + const Component = () => { + const client = React.useContext(DelayContext); + if (!client) { + return 'context not found.'; + } + const result = client.get(); + if (typeof result === 'string') { + return result; + } + throw result; + }; + + const client = new DelayClient(); + const {writable, output, completed} = getTestWritable(); + ReactDOMFizzServer.renderToPipeableStream( + + + + + , + ).pipe(writable); + + jest.runAllTimers(); + + expect(output.error).toBe(undefined); + expect(output.result).toContain('loading'); + + await completed; + + expect(output.error).toBe(undefined); + expect(output.result).not.toContain('context never found'); + expect(output.result).toContain('OK'); + }); + + // @gate experimental + it('should be able to get context value when calls renderToPipeableStream twice at the same time', async () => { + class DelayClient { + get() { + if (this.resolved) return this.resolved; + if (this.pending) return this.pending; + return (this.pending = new Promise(resolve => { + setTimeout(() => { + delete this.pending; + this.resolved = 'OK'; + resolve(); + }, 500); + })); + } + } + const DelayContext = React.createContext(undefined); + const Component = () => { + const client = React.useContext(DelayContext); + if (!client) { + return 'context never found'; + } + const result = client.get(); + if (typeof result === 'string') { + return result; + } + throw result; + }; + + const client0 = new DelayClient(); + const { + writable: writable0, + output: output0, + completed: completed0, + } = getTestWritable(); + ReactDOMFizzServer.renderToPipeableStream( + + + + + , + ).pipe(writable0); + + const client1 = new DelayClient(); + const { + writable: writable1, + output: output1, + completed: completed1, + } = getTestWritable(); + ReactDOMFizzServer.renderToPipeableStream( + + + + + , + ).pipe(writable1); + + jest.runAllTimers(); + + expect(output0.error).toBe(undefined); + expect(output0.result).toContain('loading'); + + expect(output1.error).toBe(undefined); + expect(output1.result).toContain('loading'); + + await Promise.all([completed0, completed1]); + + expect(output0.error).toBe(undefined); + expect(output0.result).not.toContain('context never found'); + expect(output0.result).toContain('OK'); + + expect(output1.error).toBe(undefined); + expect(output1.result).not.toContain('context never found'); + expect(output1.result).toContain('OK'); + }); + + // @gate experimental + it('should be able to pop context after suspending', async () => { + class DelayClient { + get() { + if (this.resolved) return this.resolved; + if (this.pending) return this.pending; + return (this.pending = new Promise(resolve => { + setTimeout(() => { + delete this.pending; + this.resolved = 'OK'; + resolve(); + }, 500); + })); + } + } + + const DelayContext = React.createContext(undefined); + const Component = () => { + const client = React.useContext(DelayContext); + if (!client) { + return 'context not found.'; + } + const result = client.get(); + if (typeof result === 'string') { + return result; + } + throw result; + }; + + const client = new DelayClient(); + const {writable, output, completed} = getTestWritable(); + ReactDOMFizzServer.renderToPipeableStream( + <> + + + + + + + + + + + , + ).pipe(writable); + + jest.runAllTimers(); + + expect(output.error).toBe(undefined); + expect(output.result).toContain('loading'); + + await completed; + + expect(output.error).toBe(undefined); + expect(output.result).not.toContain('context never found'); + expect(output.result).toContain('OK'); + }); }); diff --git a/packages/react-server/src/ReactFizzNewContext.js b/packages/react-server/src/ReactFizzNewContext.js index 0cb879ef780d6..0eaa07f839a16 100644 --- a/packages/react-server/src/ReactFizzNewContext.js +++ b/packages/react-server/src/ReactFizzNewContext.js @@ -78,9 +78,10 @@ function popToNearestCommonAncestor( } popToNearestCommonAncestor(parentPrev, parentNext); - // On the way back, we push the new ones that weren't common. - pushNode(next); } + + // On the way back, we push the new ones that weren't common. + pushNode(next); } }