Issue 1:
We use a library called Fresnel to achieve the following

Render markup for all breakpoints on the server and send it down the wire.

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.

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.

Latest compatible version: 18.0.0-rc.0-next-fa816be7f-20220128

First incompatible version: 18.0.0-rc.0-next-3a4462129-20220201

Most recent versions are still incompatible

Identified changes short after the last compatible version was published.

I did some digging into the recent changes in React and may have been able to identify the problem.

The initial report artsy/fresnel#260 (comment) 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.

3f5ff16

In the same commit, further changes are made, with at least the following leading to another problem (assuming error throwing is disabled).

react/packages/react-reconciler/src/ReactFiberHydrationContext.old.js

Line 563
in
3f5ff16

} else {

if (nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
warnIfUnhydratedTailNodes(fiber);
throwOnHydrationMismatchIfConcurrentMode(fiber); // => (*2)
}
else { // => (*1)
while (nextInstance) {
deleteHydratableInstance(fiber, nextInstance);
nextInstance = getNextHydratableSibling(nextInstance);
}
}
}

Local tests show that the condition statement "if/else" block is wrong; the delete operation must always be executed.

// *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.

// *2 Throwing errors have to be optional, not mandatory, options to think about

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

Disable enableClientRenderFallbackOnHydrationMismatch when suppressHydrationWarning is set.

Note: When looking at the associated hydration test suite https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js 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.

it('with if / else in place', async () => {
function App({hasA, hasB}) {
return (
<div>
<div>{hasA ? <span>A</span> : null}</div>
<div>{hasB ? <span>B</span> : null}</div>
</div>
);
}

const finalHTML = ReactDOMServer.renderToString(
<App hasA={true} hasB={true} />, // 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,
<App hasA={true} hasB={false} />,
);

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

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

root.render(<App hasA={false} hasB={true} />);

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

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

root.render(<App hasA={true} hasB={false} />);

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

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

Maybe you guys can give some feedback if the identified problem in those changes made is really the cause to the problem.

Thx!

Issue 2:
Do you want to request a feature or report a bug?

bug

What is the current behavior?

If a component performs a second (non user triggered) update, act cannot detect it and warns about the update.

For example, a button is clicked and updates its text. After a second, the button resets and its text reverts to its original state.

https://codesandbox.io/s/6xkyl37x7k?previewwindow=tests

(The reproduction is a bit contrived, but demonstrates the issue.)

What is the expected behavior?

The test runs without warning about being wrapped in act.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

React & React DOM @ 16.8.0