<p dir="auto">We use a library called Fresnel to achieve the following</p>
<ol dir="auto">
<li>Render markup for all breakpoints on the server and send it down the wire.</li>
<li>The browser receives markup with proper media query styling and will immediately start rendering the expected visual result for whatever viewport width the browser is at.</li>
<li>When all JS has loaded and React starts the rehydration phase, we query the browser for what breakpoint it’s currently at and then limit the rendered components to the matching media queries. This prevents life-cycle methods from firing in hidden components and unused html being re-written to the DOM.</li>
</ol>
<p dir="auto">Latest compatible version: 18.0.0-rc.0-next-fa816be7f-20220128<br>
First incompatible version: 18.0.0-rc.0-next-3a4462129-20220201<br>
Most recent versions are still incompatible</p>
<p dir="auto">Identified changes short after the last compatible version was published.</p>
<p dir="auto">I did some digging into the recent changes in React and may have been able to identify the problem.</p>
<p dir="auto">The initial report <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="1143001845" data-permission-text="Title is private" data-url="https://github.com/artsy/fresnel/issues/260" data-hovercard-type="issue" data-hovercard-url="/artsy/fresnel/issues/260/hovercard?comment_id=1054122815&amp;comment_type=issue_comment" href="https://github.com/artsy/fresnel/issues/260#issuecomment-1054122815">artsy/fresnel#260 (comment)</a> describes an error thrown when server-side generated components no longer match those on the client-side. This change of application behavior was introduced in the following commit.</p>
<p dir="auto"><a class="commit-link" data-hovercard-type="commit" data-hovercard-url="https://github.com/facebook/react/commit/3f5ff16c1a743bce3f3d772dc5ac63c5fd2dac0a/hovercard" href="https://github.com/facebook/react/commit/3f5ff16c1a743bce3f3d772dc5ac63c5fd2dac0a"><tt>3f5ff16</tt></a></p>
<p dir="auto">In the same commit, further changes are made, with at least the following leading to another problem (assuming error throwing is disabled).</p>
<p dir="auto"></p><div class="Box Box--condensed my-2">
  <div class="Box-header f6">
    <p class="mb-0 text-bold">
      <a href="https://github.com/facebook/react/blob/3f5ff16c1a743bce3f3d772dc5ac63c5fd2dac0a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js#L563">react/packages/react-reconciler/src/ReactFiberHydrationContext.old.js</a>
    </p>
    <p class="mb-0 color-fg-muted">
         Line 563
      in
      <a data-pjax="true" class="commit-tease-sha" href="/facebook/react/commit/3f5ff16c1a743bce3f3d772dc5ac63c5fd2dac0a">3f5ff16</a>
    </p>
  </div>
  <div itemprop="text" class="Box-body p-0 blob-wrapper blob-wrapper-embedded data">
    <table class="highlight tab-size mb-0 js-file-line-container" data-tab-size="8" data-paste-markdown-skip="">

        <tbody><tr class="border-0">
          <td id="L563" class="blob-num border-0 px-3 py-0 color-bg-default" data-line-number="563"></td>
          <td id="LC563" class="blob-code border-0 px-3 py-0 color-bg-default blob-code-inner js-file-line"> <span class="pl-kos">}</span> <span class="pl-k">else</span> <span class="pl-kos">{</span> </td>
        </tr>
    </tbody></table>
  </div>
</div>
<p></p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="if (nextInstance) {
  if (shouldClientRenderOnMismatch(fiber)) {
   warnIfUnhydratedTailNodes(fiber);
   throwOnHydrationMismatchIfConcurrentMode(fiber); // =&gt; (*2)
  }
  else { // =&gt; (*1)
   while (nextInstance) {
     deleteHydratableInstance(fiber, nextInstance);
     nextInstance = getNextHydratableSibling(nextInstance);
   }
  }
}"><pre class="notranslate"><code class="notranslate">if (nextInstance) {
  if (shouldClientRenderOnMismatch(fiber)) {
   warnIfUnhydratedTailNodes(fiber);
   throwOnHydrationMismatchIfConcurrentMode(fiber); // =&gt; (*2)
  }
  else { // =&gt; (*1)
   while (nextInstance) {
     deleteHydratableInstance(fiber, nextInstance);
     nextInstance = getNextHydratableSibling(nextInstance);
   }
  }
}
</code></pre></div>
<p dir="auto">Local tests show that the condition statement "if/else" block is wrong; the delete operation must always be executed.</p>
<p dir="auto">// *1 The delete operation of unmatched siblings needs to be called anyway; otherwise, DOM and React get out of sync, meaning phantom DOM entries (duplicated DOM Elements) get generated when re-rendering occurs. Those elements do not have a corresponding react component in the dev tools.</p>
<p dir="auto">// *2 Throwing errors have to be optional, not mandatory, options to think about</p>
<p dir="auto">Remove throwing errors altogether; at least make it optional because the third argument in hydrateRoot is not used/implemented by any consumer of this API, such as in NextJS, although they promise you can use the latest experimental React version<br>
Disable enableClientRenderFallbackOnHydrationMismatch when suppressHydrationWarning is set.</p>
<p dir="auto">Note: When looking at the associated hydration test suite <a href="https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js">https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js</a> it is noticeable that in the tests mostly suspense is used. In the following test (no test exclusively output after hydration, first and second render run) simple elements are used.</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="it('with if / else in place', async () =&gt; {
  function App({hasA, hasB}) {
    return (
      &lt;div&gt;
        &lt;div&gt;{hasA ? &lt;span&gt;A&lt;/span&gt; : null}&lt;/div&gt;
        &lt;div&gt;{hasB ? &lt;span&gt;B&lt;/span&gt; : null}&lt;/div&gt;
      &lt;/div&gt;
    );
  }

  const finalHTML = ReactDOMServer.renderToString(
    &lt;App hasA={true} hasB={true} /&gt;, // Render markup for all breakpoints on the server and send it down the wire.
  );

  const container = document.createElement('div');
  container.innerHTML = finalHTML;

  const root = ReactDOM.hydrateRoot(
    container,
    &lt;App hasA={true} hasB={false} /&gt;,
  );

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong): =&gt; &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'after hydration / hasA={true} hasB={false}:',
    container.innerHTML,
  );

  root.render(&lt;App hasA={false} hasB={true} /&gt;);

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong - phantom elements created): =&gt; &lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'first re-render / hasA={false} hasB={true}:',
    container.innerHTML,
  );

  root.render(&lt;App hasA={true} hasB={false} /&gt;);

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'second re-render / hasA={true} hasB={false}:',
    container.innerHTML,
  );
});"><pre class="notranslate"><code class="notranslate">it('with if / else in place', async () =&gt; {
  function App({hasA, hasB}) {
    return (
      &lt;div&gt;
        &lt;div&gt;{hasA ? &lt;span&gt;A&lt;/span&gt; : null}&lt;/div&gt;
        &lt;div&gt;{hasB ? &lt;span&gt;B&lt;/span&gt; : null}&lt;/div&gt;
      &lt;/div&gt;
    );
  }

  const finalHTML = ReactDOMServer.renderToString(
    &lt;App hasA={true} hasB={true} /&gt;, // Render markup for all breakpoints on the server and send it down the wire.
  );

  const container = document.createElement('div');
  container.innerHTML = finalHTML;

  const root = ReactDOM.hydrateRoot(
    container,
    &lt;App hasA={true} hasB={false} /&gt;,
  );

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong): =&gt; &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'after hydration / hasA={true} hasB={false}:',
    container.innerHTML,
  );

  root.render(&lt;App hasA={false} hasB={true} /&gt;);

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong - phantom elements created): =&gt; &lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'first re-render / hasA={false} hasB={true}:',
    container.innerHTML,
  );

  root.render(&lt;App hasA={true} hasB={false} /&gt;);

  jest.runAllTimers();
  Scheduler.unstable_flushAll();

  /**
   * Results:
   * 1. current version if / else in place (wrong): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
   * 2. only one if - delete on default (expected): &lt;div&gt;&lt;div&gt;&lt;span&gt;A&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
   */
  console.log(
    'second re-render / hasA={true} hasB={false}:',
    container.innerHTML,
  );
});
</code></pre></div>
<p dir="auto">Maybe you guys can give some feedback if the identified problem in those changes made is really the cause to the problem.</p>
<p dir="auto">Thx!</p>
<p dir="auto"><strong>Do you want to request a <em>feature</em> or report a <em>bug</em>?</strong></p>
<p dir="auto">bug</p>
<p dir="auto"><strong>What is the current behavior?</strong></p>
<p dir="auto">If a component performs a second (non user triggered) update, <code class="notranslate">act</code> cannot detect it and warns about the update.</p>
<p dir="auto">For example, a button is clicked and updates its text. After a second, the button resets and its text reverts to its original state.</p>
<p dir="auto"><a href="https://codesandbox.io/s/6xkyl37x7k?previewwindow=tests" rel="nofollow">https://codesandbox.io/s/6xkyl37x7k?previewwindow=tests</a></p>
<p dir="auto">(The reproduction is a bit contrived, but demonstrates the issue.)</p>
<p dir="auto"><strong>What is the expected behavior?</strong></p>
<p dir="auto">The test runs without warning about being wrapped in <code class="notranslate">act</code>.</p>
<p dir="auto"><strong>Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?</strong></p>
<p dir="auto">React &amp; React DOM @ <code class="notranslate">16.8.0</code></p>