Skip to content

React 18: Context providers are reset to initial value in SSR during rendering #23089

Closed

Description

@frandiox

While testing SSR streaming in latest React 18 experimental and alpha versions, we noticed that context providers are reset to their initial values during rendering under certain conditions.
It works well when handling 1 request at a time. However, when the server gets 2 or more requests at the same time, the context providers seem to get confused. The context is correct at the beginning of the rendering for each request but it gets lost after a while.

There's a reproduction here using @gaearon 's demo: https://codesandbox.io/s/keen-snowflake-8nyo8?file=/src/data.js:1035-1082

To my understanding, since the React tree is wrapped in a provider in SSR, useContext should never return null in the server. Have a look at the terminal and see how it actually logs null sometimes when getting multiple requests.

[0] This should never be null: { read: [Function: read] }
[0] This should never be null: { read: [Function: read] }
[0] This should never be null: null
[0] This should never be null: null

Run the following code from the console to simulate multiple requests:

function doRequest() { return fetch('https://8nyo8.sse.codesandbox.io/', {headers: {accept:'text/html'}}).then(r => r.text()) }
await Promise.all([doRequest(), doRequest()])

We saw this same issue in different setups, using both Webpack and Vite.

Thanks!

Activity

  1. zythum commented on Jan 13, 2022

    @zythum

    yes, i got the same problem. when got more then 2 requests at the same time, the context providers will not work.

    this is my code:

    react / react-dom version is 18.0.0-rc.0-next-9a7e6bf0d-20220111

    import http from "http";
    import React from "react";
    import ReactDOMServer from "react-dom/server";
    
    class DelayClient {
      public resolved?: string;
      public pending?: Promise<void>
      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();
            console.log("timeout");
          }, 2000);
        });
      }
    
    }
    const DelayContext = React.createContext<DelayClient | undefined>(undefined);
    
    // this is the component that use delay
    // when a single request coming, this is OK. delay 2 seconds then got a "OK" from context
    // but if mutli request (refresh website fastly), it will delay 2 seconds then context is undefined.
    const Component: React.FC = () => {
      const client = React.useContext(DelayContext);
      if (!client) {
        return <div>context not found.</div>;
      }
      const result = client.get();
      if (typeof result === "string") {
        return <div>{result}</div>
      }
      throw result;
    }
    
    const app = http.createServer((req, res) => {
    
      if (req.url !== "/") {
        res.statusCode = 200;
        res.end();
        return;
      }
    
      // every request has a DelayClient to wait 2 seconds
      // then get a "OK" from DelayClient through context
      const client = new DelayClient();
      const element = <html>
        <head>
          <title>sample</title>
        </head>
        <body>
          <DelayContext.Provider value={client}>
            <React.Suspense fallback="loading">
              <Component/>
            </React.Suspense>
          </DelayContext.Provider>
        </body>
      </html>;
      const { pipe } = (ReactDOMServer as any).renderToPipeableStream(element, {
        onError(error: any) {
          console.log("error", error);
        },
        onCompleteShell() {
          res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
          pipe(res);
        },
      });
    });
    app.listen(3000, () => {
      console.log("app lift: http://127.0.0.1:3000/")
    });
  2. gaearon commented on Jan 13, 2022

    @gaearon
    Contributor

    Can you turn this into a failing test? Search the tests for existing tests using renderToPipeableStream.

  3. zythum commented on Jan 18, 2022

    @zythum

    Failing test here. #23129 @gaearon

  4. sebmarkbage commented on Jan 20, 2022

    @sebmarkbage
    Contributor

    It's supposed to get reset here when restarting work after switching between working on different tasks (whether they're part of the same request or multiple). https://github.com/facebook/react/blob/ebf9ae8579230e7b1ed0b1d243e1cf802f56938b/packages/react-server/src/ReactFizzServer.js#L1502

  5. added a commit that references this issue on Jan 24, 2022
    d49a09e
  6. added a commit that references this issue on Jan 24, 2022
    ff58d45
  7. added a commit that references this issue on Jan 24, 2022
    529dc3c
  8. gaearon commented on Jan 24, 2022

    @gaearon
    Contributor

    The fix is out in 18.0.0-rc.0-next-529dc3ce8-20220124.

  9. added a commit that references this issue on Apr 15, 2022
    f6caca7
  10. added a commit that references this issue on Apr 22, 2022
    1e2a0d4
  11. added a commit that references this issue on Jun 25, 2026
    d49594d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    React 18Bug reports, questions, and general feedback about React 18Type: Bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions