Upgrading to React 18 and common pitfalls of concurrent mode

WordPress 6.2 ships with version 18 of ReactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/., the JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. library used to build the BlockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. Editor and all custom blocks. It comes with several new features, improvements, and bugbug A bug is an error or unexpected result. Performance improvements, code optimization, and are considered enhancements, not defects. After feature freeze, only bugs are dealt with, with regressions (adverse changes from the previous version) being the highest priority. fixes, including a new rendering algorithm, called concurrent mode. (#45235)

In concurrent mode, React performs UIUI User interface updates faster, and keeps the web page responsive. When it works on a large and complex UI update, it can still process all user input (mouse events, scrolling, keyboard events) in real-time, concurrently with the work it’s already doing.

However, it also introduces some potential pitfalls that developers need to be aware of, and which may break some components that rely on the precise timing of events and state updates. These pitfalls affect only a small set of complex and specialized React code. Unless your code relies on the specific timing of state updates, it’s almost certain that your code will continue to work without any changes.

Batched state updates

Almost all concurrent mode pitfalls are related to a feature called “batched state updates”. What does that mean? Consider this React component:

function ShowX() {
  const [ x, setX ] = useState( 0 );

  console.log( 'rendering with state', x );

  useEffect( () => {
    const handle = setTimeout( () => {
      console.log( 'started setting state' );
      setX( 1 );
      setX( 2 );
      console.log( 'finished setting state' );
    }, 1000 );

    return () => clearTimeout( handle );
  }, [] );

  return <div>{ x }</div>;
}

This component will initially render with state 0, and after one second it will do two state updates after each other: first to 1 and then to 2. In React 17, without concurrent mode and automated batching, messages in the console would be logged in this order:

rendering with state 0
started setting state
rendering with state 1
rendering with state 2
finished setting state

Each of the setX calls will immediately and synchronously trigger a component render, and there will be two renders. By the time the script executes the line that logs finished setting state, both renders have already happened. The effects from the setX(1) update has been executed, too. Every so often there is code that relies on the fact that the render and/or effects are already performed at this moment. And exactly this kind of code is a typical source of concurrent mode bugs. Because in concurrent mode, in React 18, the order of the logged messages will be very different:

rendering with state 0
started setting state
finished setting state
rendering with state 2

First, by the time the finished setting state message is being logged, no render has happened yet. At that time it’s merely scheduled, not yet performed.

Second, both setX(1) and setX(2) updates have been batched together, and only one render was performed with the 2 final values, after performing both state updates in a batch. That’s another source of bugs. If your code relied on the render with the state 1 being performed, it will never happen. Effects are also running only with the 2 value, the 1 effects are skipped.

Batched updates and @wordpress/data

A special case of batched state updates, often present in WordPress code, are dispatch calls in @wordpress/data stores:

const counter = useDispatch( counterStore );
counter.increment();

Here, dispatching the increment action ultimately leads to a state update inside a component that selects from the counterStore. Occasionally, your code can rely on the fact that immediately after the counter.increment() call, all the updates and re-renders have been already synchronously executed. But, as described above, in React 18 concurrent mode that doesn’t happen immediately. The update is merely scheduled at that time.

New APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. for mounting a root

If your pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party or block is mounting its own React UI into the page, instead of exporting React components to be rendered by the Block Editor, you should be aware of the new React 18 APIs for mounting a component root. The old, React 17 way, was the render function from react-dom or from @wordpress/element:

import { render } from '@wordpress/element';

const el = document.getElementById( 'root' );
render( <App />, el );

This still continues to work, and you can continue to use it. The only downsides are that you’ll be getting a console warning about using a React 17 legacy API, and that concurrent mode is disabled in React apps mounted this way.

The React 18 way is to use the new createRoot API:

import { createRoot } from '@wordpress/element';

const el = document.getElementById( 'root' );
const root = createRoot( el );
root.render( <App /> );

There is one extra step: from a DOM element, you create a root, and then you render a JSX element into that root. Apps mounted this way will use the new concurrent mode.

There is also a new API for unmounting a React root. The old one was unmountComponentAtNode( el ), the new one is to call a method on the root object: root.unmount()

Other new APIs in React 18

There are other new API functions in React 18, all of them also exported by the @wordpress/element package:

These are entirely new and don’t introduce any backwards-compatibility concerns. If you would like to learn about them or want to use them, please consult the React 18 Migration Guide and the React docs.

Props to @youknowriad, @tyxla, @bph, @milana_cap for review

#6-2, #dev-notes, #dev-notes-6-2