WordPress / WordPress/plugin-check
Enhancement: Add a check to warn about React 19 incompatibilities / bundled outdated React
@jonathanbossenger is already working on this.
Since Jun 12, 2026.
- Dominant language
- PHP
- Stars
- 362
- Forks
- 126
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 22
Description
WordPress is planning to upgrade its bundled React from 18 to 19.
Real-world compatibility testing in the Gutenberg React 19 polyfill PR (WordPress/gutenberg#78899) showed that large, widely-installed plugins (Yoast SEO, Elementor, Fluent Forms, Sure Cart, Sure Forms, etc.) break under React 19.
~99% of these failures share a single root cause: plugins inline the react/jsx-runtime library code instead of externalizing it.
The JSX runtime is used by transpiled JSX. Syntax like:
<Foo className="foo">hello</Foo>
is transpiled into:
import { jsx } from 'react/jsx-runtime';
jsx( Foo, { className: 'foo' }, 'hello' );
and at runtime jsx() returns an "element" object:
{
$$typeof: Symbol.for( 'react.element' ),
type: Foo,
props: { className: 'foo', children: 'hello' },
}
These objects are then handed to React to render. Between React 18 and 19 the element object shape changed — only slightly, but enough that React 19 rejects objects produced by the React 18 runtime. So if a plugin inlines the React 18 jsx-runtime, it produces incompatible objects and feeds them to WordPress's React 19.
The fix is to always externalize the JSX runtime. WordPress provides the react-jsx-runtime script handle, and bundled plugin code should reference it through the global window.ReactJSXRuntime. @wordpress/scripts / @wordpress/dependency-extraction-webpack-plugin does this automatically; the problem occurs when a plugin's build inlines the runtime instead.
Plugin Check is well placed to catch this before a plugin ships.
Proposal
Add a static-analysis check that scans a plugin's built JavaScript for an inlined JSX runtime, and passes plugins that correctly externalize it.
Primary signal — inlined JSX runtime (high confidence)
The inlined runtime bakes the element tag into the bundle as a literal Symbol.for() call. The string argument survives minification (it is a runtime value, not a renameable identifier), so it is a reliable fingerprint:
Symbol.for("react.element")— present when a pre-19 JSX runtime is inlined. React 19's runtime usesSymbol.for("react.transitional.element"), so finding the non-transitional string is strong evidence of an inlined React 18-era runtime being fed to React 19. This is the headline detector.- Supporting fingerprints (optional, to raise confidence): the runtime's exported helpers
jsx,jsxs,jsxDEVdefined inline alongside theSymbol.for("react.element")tag.
"All clear" / suppression — correctly externalized
A properly externalized plugin will not contain the inlined runtime. Use these as suppression signals to keep false positives low:
- The generated
*.asset.phpdependency array listsreact-jsx-runtime(and/orreact,react-dom) — the runtime is provided by core, not bundled. - The built JS references
window.ReactJSXRuntime(the external global the runtime is mapped to).
If either is present and no inlined-runtime fingerprint is found, the plugin is doing it right.
Secondary signal — removed React APIs (lower volume)
A minority of failures came from removed/changed APIs rather than the runtime. These are distinctive tokens that also survive minification and can be reported with their own message:
| Token | Issue | Guidance |
|---|---|---|
unmountComponentAtNode |
Removed in React 19 | Use root.unmount() from createRoot |
findDOMNode |
Removed in React 19 | Use refs |
hydrate (ReactDOM.hydrate) |
Removed in React 19 | Use hydrateRoot |
ReactCurrentOwner |
Internal, restructured in React 19 | Don't access React internals |
(ReactDOM.render / bare .render( should be excluded — too noisy in minified bundles.)
Message / guidance
When the inlined runtime is detected, the message should point at the real fix, e.g.:
This file appears to inline the React JSX runtime (
react/jsx-runtime), which is incompatible with WordPress's React 19. Externalize it instead and reference WordPress'sreact-jsx-runtimescript (window.ReactJSXRuntime). When building with@wordpress/scripts, this happens automatically.
Link to the React 19 upgrade post.
Severity
Warning, not error — detection is heuristic (minification, bundler differences, custom builds), and while inlining the runtime is the wrong choice for WordPress, it is not strictly forbidden. (Open to making it an error given how high-confidence the primary signal is — see open questions.)
Implementation notes
Fits the existing check architecture with no new infrastructure:
- New class under
includes/Checker/Checks/Plugin_Repo/(orPerformance/) extendingAbstract_File_Check, using theStable_CheckandAmend_Check_Resulttraits — same shape asFile_Type_Check, which already fingerprints bundled libraries (jQuery, Backbone, Underscore) by pattern. - Logic in
check_files():filter_files_by_extension( $files, 'js' ), thenfiles_preg_match_all()forSymbol.for("react.element")(allowing for whitespace/quote variations). Optionally read sibling*.asset.phpfiles to apply the externalization suppression. - Report via
add_result_warning_for_file()with file + line and adocslink. - Register the slug in
Default_Check_Repository.php. - Add a test under
tests/phpunit/tests/Checker/Checks/with fixture plugins intests/phpunit/testdata/plugins/: one with an inlinedSymbol.for("react.element")runtime (should warn), and one that externalizes viareact-jsx-runtime/window.ReactJSXRuntime(should pass). MirrorFile_Type_Check_Tests.
Open questions
- Regex robustness — confirm
Symbol.for("react.element")survives across the common build pipelines (@wordpress/scripts, CRA, Vite, esbuild) and quote/whitespace variants. Worth grepping a couple of the known-broken plugin builds (Yoast, Elementor) to validate before finalizing the pattern. - Error vs. warning — given the primary signal is high-confidence and the impact is "breaks on current WordPress," should this be an error rather than a warning?
- Category — Plugin_Repo, Performance, or a dedicated compatibility bucket?
- Transitional tag — should we also flag
react.transitional.elementanywhere, or is its presence harmless (it indicates a React 19 runtime, which is fine, though inlining even the v19 runtime is still suboptimal vs. externalizing)?
References
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.