Pattern creator: use createRoot for React 18/19 forward compatibility#746
Merged
Conversation
The pattern editor mounted via `render()` and tore down via `unmountComponentAtNode()` from `@wordpress/element`. React 19 removed `ReactDOM.render` and `unmountComponentAtNode`, and the corresponding `@wordpress/element` wrappers go away with it, so the editor would fail to mount once WordPress serves React 19. Switch to the `createRoot` API, holding the root in module scope so the reboot-after-error flow can `unmount()` and re-create it. `createRoot` is the React 18+ API and is exported by `@wordpress/element` today, so this works under both React 18 and 19 with no runtime version pinning. The unmount → updateSettings → render ordering of the reboot path is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the Pattern Creator editor bootstrap to use React’s modern root API so the editor can mount/unmount correctly when WordPress serves React 19 (which removes legacy ReactDOM.render-style APIs).
Changes:
- Replaces legacy
render/unmountComponentAtNodeusage withcreateRoot+root.render()/root.unmount(). - Stores the React root in module scope to support the “reboot after error” flow.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The createRoot change fixed React 19 removing ReactDOM.render, but the editor still threw React error #525 ("A React Element from an older version of React was rendered") under React 19. @wordpress/scripts compiles JSX with the automatic runtime (react/jsx-runtime). The pinned dependency-extraction-webpack-plugin (5.2.0) only externalizes `react` and `react-dom`, not `react/jsx-runtime`, so webpack bundled the local React 18 JSX runtime. Its jsx() stamps elements with the legacy `react.element` type, which React 19's react-dom rejects. Externalize react/jsx-runtime (and the dev variant) to WordPress's `react-jsx-runtime` handle / ReactJSXRuntime global, matching what current @wordpress/scripts does by default. Version-agnostic: the handle always resolves to the runtime's own React.
Bumps @wordpress/scripts to 32.3.0 and dependency-extraction-webpack-plugin to 6.47.0 across all workspaces (root, pattern-creator, pattern-directory, theme). The newer tooling externalizes react/jsx-runtime to WordPress's runtime `react-jsx-runtime` handle by default, so the editor's JSX is created by the runtime's React (18 or 19) instead of a bundled React 18 copy. This fixes React error #525 under React 19 and supersedes the manual externalization in 75fccc9, which is removed. Adopts @wordpress/scripts' default ESLint config (removes the root .eslintrc.js and the per-workspace eslintConfig blocks). Fallout fixed: - declare the @wordpress/* packages the theme imports; add @wordpress/private-apis to the creator - /* global */ directives for the PHP-injected wporgBlockPattern/wporgLocale - object-shorthand, i18n translator-comment placeholders, optional catch bindings, and one assign-before-return - SCSS z-index key rename for @wordpress/base-styles 9.x - jest TextEncoder/TextDecoder polyfill (needed by newer deps under jsdom) Pins wpackagist-plugin/gutenberg to 23.3.0 to run a React 19 runtime locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the agent-guidance doc to the tool-agnostic AGENTS.md (dropping the Claude-specific intro line) and leaves CLAUDE.md as a one-line `@AGENTS.md` import so both conventions resolve to the same content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The @wordpress/scripts 32.3 upgrade dropped the per-workspace eslintConfig blocks and the legacy root .eslintrc.js. Add a root eslint.config.js that extends the wp-scripts default flat config and re-adds the project text domain to the @wordpress/i18n-text-domain rule, so a wrong-but-present domain (e.g. a typo) is caught at lint time again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The editor bootstrap now uses `createRoot` from `@wordpress/element`, which was first exposed by WordPress 6.2. Bump the plugin's "Requires at least" header from 5.5 to 6.2 so it accurately reflects the runtime API the entrypoint depends on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
reinitializeEditor() destructures postId out of its settings argument via
{ postId, ...settings }, but the reboot handler was bound with only the rest
object. On the error-reboot path it therefore reinitialized with
postId === undefined, leaving the remounted editor unable to resolve the
post and rendering a blank screen. Bind the full { postId, ...settings } so
a reboot reinitializes the same post.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The workspaces now resolve linting through the root eslint.config.js (ESLint 9 flat config), not the removed .eslintrc.js. Correct the docs so contributors setting up linting reference the right file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
React 19 removes
ReactDOM.render/unmountComponentAtNode(and the@wordpress/elementwrappers for them), so the pattern creator's editor bootstrap would fail to mount once WordPress serves React 19. This branch makes the project React-19-ready: it migrates the one incompatible mount path tocreateRoot, and brings the build tooling / Gutenberg version up to a React-19 baseline.The
createRootAPI is the React 18+ API exported by@wordpress/elementtoday and is forward-compatible with both React 18 and 19 — no runtime React pinning.What changed
Editor mount (the core fix)
import { render, unmountComponentAtNode }→import { createRoot }inpattern-creator/src/index.js.unmount()and re-create it.unmount → updateSettings → renderordering of the reboot path is preserved.react/react-jsx-runtimeare externalized so they're provided by WordPress at runtime.Build tooling / dependencies
@wordpress/scripts30.7 → 32.3 (ESLint 9 / flat config),@wordpress/dependency-extraction-webpack-plugin5.2 → 6.47,@wordpress/stylelint-config23.6 → 23.39.*→ 23.3.0 (React 19.2.4) incomposer.json/composer.lock— the React-19 target.@wordpress/private-apisto the creator and the runtime@wordpresspackages the theme externalizes.TextEncoder/TextDecoder(jest.setup.js) which newer@wordpressdeps require at module load.ESLint flat-config migration
eslintConfigblocks and the legacy root.eslintrc.jsare replaced by a single rooteslint.config.jsthat extends the@wordpress/scriptsdefault flat config.@wordpress/i18n-text-domainrule (allowedTextDomain: ['wporg-patterns']) so i18n calls are still validated againstwporg-patternsrather than the default domain. (Lint-time only — runtime translations are unaffected; they rely on the domain hardcoded in each call +wp_set_script_translations().)wporgBlockPattern,wporgLocale) are now declared via/* global */comments in the files that use them.page: page→page),catch (error)→catchwhere the binding was unused, and correctedtranslators:placeholder comments (%d→ positional%1$d/%1$s).Scope of the React 19 review
A full sweep of the project's JS (
pattern-creator,pattern-directory, theme) found the editor mount was the only React 19 incompatibility. No occurrences of:findDOMNode, string refs, function-componentdefaultProps, legacy context (contextTypes/getChildContext),propTypes/prop-types,react-test-renderer,createFactory, or callback refs returning non-cleanup values. No workspace directly depends onreact/react-dom.Testing
npm run lint:js --workspaces— 0 errors across all three workspaces (pre-existingexhaustive-depswarnings unchanged). Verified the restored@wordpress/i18n-text-domainrule still fires: a probe using a wrong text domain errors as expected.npm run build:creator— compiles successfully.🤖 Generated with Claude Code