Skip to content

Pattern creator: use createRoot for React 18/19 forward compatibility#746

Merged
obenland merged 8 commits into
trunkfrom
fix/react-19-forward-compat
Jun 5, 2026
Merged

Pattern creator: use createRoot for React 18/19 forward compatibility#746
obenland merged 8 commits into
trunkfrom
fix/react-19-forward-compat

Conversation

@obenland

@obenland obenland commented Jun 4, 2026

Copy link
Copy Markdown
Member

What & why

React 19 removes ReactDOM.render / unmountComponentAtNode (and the @wordpress/element wrappers 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 to createRoot, and brings the build tooling / Gutenberg version up to a React-19 baseline.

The createRoot API is the React 18+ API exported by @wordpress/element today 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 } in pattern-creator/src/index.js.
  • The React root is held in module scope so the reboot-after-error flow can unmount() and re-create it.
  • The unmount → updateSettings → render ordering of the reboot path is preserved.
  • react/react-jsx-runtime are externalized so they're provided by WordPress at runtime.

Build tooling / dependencies

  • @wordpress/scripts 30.7 → 32.3 (ESLint 9 / flat config), @wordpress/dependency-extraction-webpack-plugin 5.2 → 6.47, @wordpress/stylelint-config 23.6 → 23.39.
  • Gutenberg pinned *23.3.0 (React 19.2.4) in composer.json/composer.lock — the React-19 target.
  • Added @wordpress/private-apis to the creator and the runtime @wordpress packages the theme externalizes.
  • Jest: polyfill TextEncoder/TextDecoder (jest.setup.js) which newer @wordpress deps require at module load.

ESLint flat-config migration

  • ESLint 9 uses flat config. The per-workspace eslintConfig blocks and the legacy root .eslintrc.js are replaced by a single root eslint.config.js that extends the @wordpress/scripts default flat config.
  • It re-adds the project's text domain to the @wordpress/i18n-text-domain rule (allowedTextDomain: ['wporg-patterns']) so i18n calls are still validated against wporg-patterns rather than the default domain. (Lint-time only — runtime translations are unaffected; they rely on the domain hardcoded in each call + wp_set_script_translations().)
  • The previously package-local globals (wporgBlockPattern, wporgLocale) are now declared via /* global */ comments in the files that use them.
  • ESLint 9 autofixes applied across the touched files: object shorthand (page: pagepage), catch (error)catch where the binding was unused, and corrected translators: 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-component defaultProps, legacy context (contextTypes/getChildContext), propTypes/prop-types, react-test-renderer, createFactory, or callback refs returning non-cleanup values. No workspace directly depends on react/react-dom.

Testing

  • npm run lint:js --workspaces0 errors across all three workspaces (pre-existing exhaustive-deps warnings unchanged). Verified the restored @wordpress/i18n-text-domain rule still fires: a probe using a wrong text domain errors as expected.
  • npm run build:creator — compiles successfully.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings June 4, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / unmountComponentAtNode usage with createRoot + 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.

Comment thread public_html/wp-content/plugins/pattern-creator/src/index.js Outdated
Comment thread public_html/wp-content/plugins/pattern-creator/src/index.js
pkevan and others added 4 commits June 4, 2026 11:54
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>
@obenland obenland requested a review from pkevan June 4, 2026 12:25
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 30 changed files in this pull request and generated 1 comment.

Comment thread AGENTS.md Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 30 changed files in this pull request and generated no new comments.

@obenland obenland merged commit c91dbf3 into trunk Jun 5, 2026
4 checks passed
@obenland obenland deleted the fix/react-19-forward-compat branch June 5, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants