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

React 18: How to "wait" for concurrent mode #22836

Closed
theKashey opened this issue Nov 27, 2021 · 3 comments
Closed

React 18: How to "wait" for concurrent mode #22836

theKashey opened this issue Nov 27, 2021 · 3 comments
Labels
React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion

Comments

@theKashey
Copy link
Contributor

The question is driven by:

function App({ callback }) {
  // Callback will be called when the div is first created.
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");

const root = ReactDOM.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);

Given advice is working as expected, and waiting for callback results the expected HTML to be presented in DOM, except:

  • it does not wait for any useEffect
  • it does not guarantee that everything else is rendered, because Concurrent Mode does not guarantee this by default.
  • it cannot handle updates

Wondering, is there any way to:

  • await for all currently scheduled work to be executed
  • synchronously flush all unfinished work

It's also worth clarifying that in the given example React is working in a little synthetic environment, which is neither real browser nor a unit test. The existence of such a test should be questioned in the first place.

@theKashey theKashey added React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion labels Nov 27, 2021
@eps1lon
Copy link
Collaborator

eps1lon commented Nov 27, 2021

I think what you're looking for is act.

This API already existed since React 16.9 where you needed it for waiting for useEffect and any updates scheduled from these.

In React 18 you need act for every update (even root.render).

So what you're looking for in your code samples is probably something like

function App({ callback }) {
  // Callback will be called when the div is first created.
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");

const root = ReactDOM.createRoot(rootElement);
act(() => {
  root.render(<App />);
});
console.log('rendered');

Does this address your questions?

Further reading:

@theKashey
Copy link
Contributor Author

Does this address your questions?

Completely. But look like the current "docs" around React 18 API might require a little update.

@lunaruan lunaruan closed this as completed Dec 9, 2021
@theKashey
Copy link
Contributor Author

Well, actually there is another side of this problem, which might need a little different solution.
Let me start with a story

This issue was been created during my investigations around useId in order to create a test helper for "matching" results. And particularly this problem has been resolved, merged, and shipped to production 🎉

With some things fixed, we were able to notice that due to #22692 (comment) React is "recreating" the content of the nearest Suspense boundary. Ie destroyng the old content and creating the new. Something not visible to the eye, but causing extra CPU(Paint) work and increasing LCP.
Yet again, the problem was fixed, and in order to prevent future regressions I've added a simple hydration check. Like this

export const RouteHydrationReport: RC = () => {
	if (process.env.TARGET_IS_CLIENT) {
		// eslint-disable-next-line react/no-danger
		return <span dangerouslySetInnerHTML={{ __html: "" }} suppressHydrationWarning />;
	}

	return (
		<span>
			<span
				data-testid="route-hydration-report"
				data-route-rendered-at={process.env.TARGET_IS_CLIENT ? "client" : "server"}
			/>
		</span>
	);
};

The presence of a given marker in HTML code is an indication of correct hydration. Absence is an indication of client-side transition or incorrect hydration.
The test was written, and then test broke the build, as it was not passing in CI, working perfectly on a developer's machine.

We found that sometimes(1/10 runs or fewer) React discards nested Suspense boundaries(the ones with HTML), usually keeping only the top-one. We also found that using React(dev-tools) Profiler almost always causes the problem. That lead to understanding that any other work(third party scripts) on the page should be paused until React finishes rendering, as any modifications from third party scripts(we don't know which and why) can interrupt the hydration process.
Adding "render finished" callback as per original advice did solve the problem.

If you read it from just a little differently, then the meaning of the paragraph above is the following - one should almost wait for initial hydration to complete before doing any other work, which requires such wait to have "first-class" support.

Discovered frictions:

  • content of suspense boundary can be discarded with no error
  • mismatched useId are not reported(and updated) if used for data- attributes
  • hydration now is a multistep process
  • everything about creates implicit requirements from React to the customer code leading to this very issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
React 18 Bug reports, questions, and general feedback about React 18 Type: Discussion
Projects
None yet
Development

No branches or pull requests

3 participants