Repository: facebook/react
Pull request: Fix context providers in SSR when handling multiple requests

Closes #23089

@sebmarkbage @gaearon Thanks for the pointers. This seems to fix the issue but I'm not very well versed in the fizz code. Please have a look to see if this makes sense. 

<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

Context providers are broken in SSR when handling multiple requests. See #23089 for more information.

<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve?
-->

## How did you test this change?

Failing test submitted by @zythum in #23129 passes with this change. I've also tested a local issue we had in Hydrogen and it seems to be fixed.

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->


Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
patch: @@ -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(
+      <DelayContext.Provider value={client}>
+        <Suspense fallback="loading">
+          <Component />
+        </Suspense>
+      </DelayContext.Provider>,
+    ).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(
+      <DelayContext.Provider value={client0}>
+        <Suspense fallback="loading">
+          <Component />
+        </Suspense>
+      </DelayContext.Provider>,
+    ).pipe(writable0);
+
+    const client1 = new DelayClient();
+    const {
+      writable: writable1,
+      output: output1,
+      completed: completed1,
+    } = getTestWritable();
+    ReactDOMFizzServer.renderToPipeableStream(
+      <DelayContext.Provider value={client1}>
+        <Suspense fallback="loading">
+          <Component />
+        </Suspense>
+      </DelayContext.Provider>,
+    ).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(
+      <>
+        <DelayContext.Provider value={client}>
+          <Suspense fallback="loading">
+            <Component />
+          </Suspense>
+        </DelayContext.Provider>
+        <DelayContext.Provider value={client}>
+          <Suspense fallback="loading">
+            <Component />
+          </Suspense>
+        </DelayContext.Provider>
+      </>,
+    ).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');
+  });
 });

filename: packages/react-server/src/ReactFizzNewContext.js
patch: @@ -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);
   }
 }