Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Hundreds render of Suspense child with hydration error #29922

Open
SuperOleg39 opened this issue Jun 18, 2024 · 5 comments
Open

Bug: Hundreds render of Suspense child with hydration error #29922

SuperOleg39 opened this issue Jun 18, 2024 · 5 comments

Comments

@SuperOleg39
Copy link

SuperOleg39 commented Jun 18, 2024

React version: 18.2

Description

Hello!

Found a few problem cases with Suspense, one quite exotic, one easy to reproduce, and in our project I get both at the same time.

First case - hundreds render of Suspense child with hydration error.

If wrapped in Suspese component cause hydration error, React will render this component hungreds or event thousands times.

Example:

function MissmatchCmp() {
  return <p>Missmatch Component {typeof window === 'undefined' ? 1 : 2}</p>
}

function App() {
  return (
    <>
      <Suspense>
        <MissmatchCmp />
      </Suspense>
    </>
  )
}
Снимок экрана 2024-06-18 в 12 13 41

Second case - Looped render of class component with setState in constructor wrapped in Suspense

Of course, setState in counstructor sounds like a bad pattern, but we have it in some big legacy class component.

Example:

class CmpWithSetState extends React.Component {
  constructor(props) {
    super(props);

    // emulate state change after http call
    setTimeout(() => {
      this.setState({ foo: "bar" });
    // with increased timout loop can be prevented
    }, 25);
  }

  render() {
    return <p>CmpWithSetState</p>
  }
}

function App() {
  return (
    <>
      <Suspense>
        <CmpWithSetState />
      </Suspense>
    </>
  )
}
Снимок экрана 2024-06-18 в 12 22 21

Bingo case - Both of previous examples in same component are guaranteed to cause a loop

Снимок экрана 2024-06-18 в 12 28 16

profiling-data.18.06.2024.12-28-19.json.zip

Steps To Reproduce

  1. Clone repo https://github.com/SuperOleg39/react-ssr-perf-test
  2. Switch to branch react-bug-suspense-child
  3. Run application build and server - cd react-18-stream && yarn && yarn start (use Node.js 16)
  4. Open application page at http://localhost:4000

Link to code example:

SuperOleg39/react-ssr-perf-test#1

The current behavior

Components which are not really suspended, rendered multiple times, like React wait for promise to resolve.

Also looks like React treats this components both as one when check if they are suspended or not.

The expected behavior

Expect the same behaviour as without Suspense boundary.

We use Suspense for our components mostly to prevent server rendering failure if one of this components failed.

@SuperOleg39 SuperOleg39 added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Jun 18, 2024
@eps1lon
Copy link
Collaborator

eps1lon commented Jun 25, 2024

If wrapped in Suspese component cause hydration error, React will render this component hungreds or event thousands times.

This is intended behavior. React will re-render on hydration mismatches inside Suspense in an attempt to recover. This should not break any app behavior. Though the number here is quite high. What are the steps to repro this behavior in the repo you linked?

The second case is violating the Rules of React. Class component constructors must be pure i.e. calling setTimeout in them is not allowed and leads to undefined behavior.

@eps1lon eps1lon added Resolution: Needs More Information and removed Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug labels Jun 25, 2024
@SuperOleg39
Copy link
Author

SuperOleg39 commented Jun 25, 2024

This is intended behavior. React will re-render on hydration mismatches inside Suspense in an attempt to recover. This should not break any app behavior. Though the number here is quite high.

As I know React will render suspensed child multiple times when need to wait thrown promise to be resolved?

Interesting why this behaviour is applied to recoverable errors, I expected a failed client component to be called only once, then client error can be propagated to Error Boundary parent.

What are the steps to repro this behavior in the repo you linked?

The same steps from issue, but this line with class component need to be removed - https://github.com/SuperOleg39/react-ssr-perf-test/blob/react-bug-suspense-child/react-18-stream/src/App.js#L52C12-L52C27

  1. Clone repo https://github.com/SuperOleg39/react-ssr-perf-test
  2. Switch to branch react-bug-suspense-child
  3. Remove this component from App render
  4. Run application build and server - cd react-18-stream && yarn && yarn start (use Node.js 16)
  5. Open application page at http://localhost:4000
  6. Reload page with React Proiler recording
@SuperOleg39
Copy link
Author

SuperOleg39 commented Jun 25, 2024

One more thing, @eps1lon, important thing in reproduction branch - a big React component inside Suspense boundaries.

Looks like this problem occurs only when selective hydration applied to component, and it will render in concurrent mode.

For small components React did hydration fast, and probably sync.

@eps1lon
Copy link
Collaborator

eps1lon commented Jun 25, 2024

Interesting why this behaviour is applied to recoverable errors, I expected a failed client component to be called only once, then client error can be propagated to Error Boundary parent.

We retry client components one more time with sync rendering to check if we can recover.

@SuperOleg39
Copy link
Author

Hello, @eps1lon!

Do I need to send a more information about reproduction steps, or it is enough?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment