cypress: Hydration errors in Cypress when using React 18, Next, and server components

Current behavior

I’ve got a Next app running React 18. In the next.config.js, it has been configured with appDir to support Server Components. When running Cypress and accessing the page, I’m getting the following React errors:

  • (uncaught exception)Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <script> in <head>. See more info here: https://nextjs.org/docs/messages/react-hydration
  • 
(uncaught exception)Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

However, I do not see these hydration issues when accessing the page via browser, so it’s surprising to see them when running Cypress.

Steps to replicate:

  1. Clone repo and run npm install
  2. Run npm run e2e
  3. Visit http://localhost:3000 in your browser and check for console errors
  4. Run the spec app.cy.ts

Here’s what I see in Cypress: Screenshot 2023-07-05 at 16 14 00

It’s not there when visiting the app via browser: Screenshot 2023-07-05 at 16 21 08

The error will no longer appear if you remove the <script> tag on line 10 of layout.tsx (link to repro code) Screenshot 2023-07-05 at 16 14 33

Seeing as this script tag isn’t causing errors when accessing via the browser, I can only conclude that it’s being caused by how Cypress is loading this page when React Server Components are present

The error occurs both when the app is running in dev mode, and when you visit the application in production mode after running build and start. In production mode, the errors are minified:

  • (uncaught exception)Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418
  • (uncaught exception)Error: Minified React error #423; visit https://reactjs.org/docs/error-decoder.html?invariant=423

I’ve seen this issue raised in a few places. The suggestion I usually see is to prevent the tests from failing due to this uncaught exception (eg here), or to catch the issue in the application (Cypress docs)

As far as I have managed to understand, there should never be a hydration mismatch in a server rendered component since there is no client rendering to match it to. And when accessing the app via the browser, there are no mismatch errors. So I can only conclude that the issue stems from how Cypress is loading the page.

Desired behavior

Cypress should not be causing uncaught exceptions. Any exceptions that occur should also be triggered under the same conditions via browser

Test code to reproduce

https://github.com/liambutler/cypress-react-next-hydration-issue

Cypress Version

12.16.0

Node version

18.16.0

Operating System

MacOS 13.4.1

Debug Logs

No response

Other

Edit 8th November 2023- since updating Next.js from 13.4.2 to 13.5.6, we are also seeing this uncaught exception:

This also only appears when Cypress is being injected into the browser

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 24
  • Comments: 28 (13 by maintainers)

Most upvoted comments

Tested example repo with latest 18.3.0-canary-9ba1bbd65-20230922, hydration error still present.

Workaround

Cypress.on("uncaught:exception", (err) => {
  // Cypress and React Hydrating the document don't get along
  // for some unknown reason. Hopefully, we figure out why eventually
  // so we can remove this.
  if (
    /hydrat/i.test(err.message) ||
    /Minified React error #418/.test(err.message) ||
    /Minified React error #423/.test(err.message)
  ) {
    return false;
  }
});

One important note regarding this, it should be in the e2e.ts in the cypress/support directory. Otherwise it won’t work

Hi folks, I’ve confirmed that the modification suggested by @brian-mann to have Cypress clean up the script element as the last step in its execution does not solve this issue, though I think it will be part of the final solution.

Reading through the thread in the React repo that @karlhorky linked to (https://github.com/facebook/react/issues/24430) was interesting, in that people reported hydration errors with the likes of Loom and other browser extensions that modify the DOM, not just when testing with Cypress.

This makes sense based on the the nature of the problem. And it seems like the React team is aware of these consequences of stricter hydration rules and has worked to mitigate them in previous releases. I’m curious what will happen when 18.3.0 is stable. One thing we can test is a recent canary build that seems to fix a similar hydration problem for one person here: https://github.com/remix-run/remix/issues/4822#issuecomment-1699577771

To summarize: We will look into the long term way for Cypress to handle this kind of error, or to at least confirm that it is handled upstream in React.

The “existing workaround” label doesn’t mean that we won’t work on it, just that there are ways to become unblocked in the meantime and allow your tests to run.

Hey @liambutler@marktnoonan brought this to my attention and I am recommending and testing out an approach where we automatically cleanup our injections before any of your react code runs, which may actually fix this issue and not require any changes on your side. We’re going to put together a POC and may want ya’ll to try out a development version of this fix to confirm it works.

This is a very simple example:

<html>
  <head>
    <script type="text/javascript">window.foo = {}; document.currentScript.remove()</script>

    <script type="text/javascript">console.log(window.foo)</script>
  </head>
</html>

Happends also if you put in the <head> tag a stylesheet link <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&display=swap" rel="stylesheet"/>

Hi Liam and Joāo, Thank you so much for the reproduction case - it was very helpful in troubleshooting. I think I have a workaround for you. Can you try to wrap the style and script elements in a React.Suspense component?

I tested this in the reproduction case along with some small edits to more closely fit your use case, and it seems to no longer throw the hydration error when running in Cypress.

<Suspense>
  <script dangerouslySetInnerHTML={{ __html : `if(window.location.pathname === '/') { window.location.replace('http://localhost:3000/redirected') }` }}></script>
  <style dangerouslySetInnerHTML={{ __html: `body { color: #676 }`}} ></style>
</Suspense>

Please let us know if that helps!

Happends also if you put in the <head> tag a stylesheet link <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&display=swap" rel="stylesheet"/>

Hi @minimit In my case, I had the same problem when using the following.

<head>
  <link
    rel="stylesheet"
    as="style"
    crossOrigin="anonymous"
    href="https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.8/static/pretendard.css"
  />
</head>

Changing the rel attribute to preload solved the problem.

<head>
  <link
    rel="preload"
    as="style"
    crossOrigin="anonymous"
    href="https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.8/static/pretendard.css"
  />
</head>

Hope this works for you !

I will also add that the workaround interferes with the tests in a way that errors might go unnoticed. Thus, I also advocate for removing the existing workaround tag since it undermines the purpose of using Cypress.

Workaround

In case you’re wondering how exactly to disable the uncaught exception errors in Cypress as Liam and João mentioned:

@liambutler in PR description: prevent the tests from failing due to this uncaught exception (eg here),

@jcmonteiro in comment 1629028523: We will likely continue to catch the warnings and discard them in our Cypress tests

…here’s an example which was working for us (add block 1 before the test code that causes the failure, add block 2 after the test code that causes the failure):

// 1. Disable Cypress uncaught exception failures from React hydration errors
Cypress.on('uncaught:exception', (err) => {
  if (
    err.message.includes('Minified React error #418') ||
    err.message.includes('Error: Minified React error #423')
  ) {
    return false;
  }
  // Enable uncaught exception failures for other errors
});

// 2. Re-enable Cypress uncaught exception failures from React hydration errors
Cypress.on('uncaught:exception', () => {});

Hi Cacie, you are welcome and thank you for the reply.

Although the <Suspense> can help, I don’t see it as a viable option for us. The reason is that our strategy for putting the “browser validation” <script> in the top-level <head> is that it can run before any JS is loaded (including the one that resolves the <Suspense>). As for the <style> element, I fear that wrapping it in a <Suspense> might lead to unforeseen issues in a larger application.

Hi Liam 👋 - based on the error and the fact that it happens based on simply including a script tag in the server rendered <head>, I have a bit of a guess about what’s going on.

The key factor may be that Cypress adds a script tag to the <head> of pages that are visited through Cypress. The Cypress <script> sets up window.Cypress and some other critical stuff, and is injected at the top of the <head> before the page is served. It’s important that it be there on initial load since other code further down the page may rely on this global being present.

The <script> tag from your RSC is at the bottom of the head. I suspect that what is happening during hydration is that, since the validator knows to expect a <script> tag, it is parsing the head and finding the one injected by Cypress, and attempting to diff that against the <script></script> from your JSX.

I don’t have a workaround at the moment besides ignoring hydration errors, which as you’ve said it not ideal, they are still there.

I’m assuming the use case here is going to be pretty common - you want to write some inline JS in your <head> so that it is active immediately on page load in your Next app?

Hitting this problem sometimes. React should respect an element’s attribute something like react-ignore-when-hydrating=true in this case.

@cacieprins would it be possible to remove the ‘existing workaround’ tag from this ticket? I believe that the exceptions are causing issues for my tests despite the Cypress.on('uncaught:exception' catch detailed above

Hi Mark! Thanks for the reply. Liam and I work together, and I might be able to provide some more context. Your explanation makes total sense. We also observe the problem when adding an empty <style> tag to <head>. From what you’ve said, it seems like the root cause is the same, i.e., Cypress’s looking for its injected <script> tag and finding something of ours instead.

We indeed want an inline JS to execute as soon as the page is loaded. For context, that inline JS will test the browser against a series of modern JS commands, immediately redirecting (location.replace) the session to a simple “please upgrade your browser” page if such commands are not supported. That provides the best experience since we don’t need to wait for all the JS to load, potentially breaking the session if the user is running on an old browser.

The <style> tag I mentioned is used to globally set the style of some elements. It is needed because the setup we have to produce our CSS design tokens strips away some properties that we then enforce back via <style dangerouslySetInnerHTML={{ __html: minifiedCss }} />