Skip to content

fix(ui-library): replace defaultProps with ES6 defaults for React 19#23317

Merged
enricobattocchi merged 3 commits into
feature/fix-gutenberg-23.3from
react19-ui-library-defaultprops
May 29, 2026
Merged

fix(ui-library): replace defaultProps with ES6 defaults for React 19#23317
enricobattocchi merged 3 commits into
feature/fix-gutenberg-23.3from
react19-ui-library-defaultprops

Conversation

@enricobattocchi
Copy link
Copy Markdown
Member

@enricobattocchi enricobattocchi commented May 29, 2026

Context

React 19 ignores defaultProps on function/forwardRef components, so props that relied on them arrive undefined and throw when dereferenced (e.g. Title's as, Button's variant), which crashed the whole Yoast metabox on the Gutenberg 23.3 RC (React 19.2.4). This moves every @yoast/ui-library component that used defaultProps to ES6 default parameters. No net behaviour change on React 18.3; it is React 19 prep for WordPress 7.1.

Verified on a React 19.2.4 site (Gutenberg 23.3-RC1): with this change the Yoast metabox, sidebar and Settings render with a clean console; without it the metabox fails to render. The follow-up render smoke test (separate PR) also passes 58/58 @yoast/ui-library components on both React 18.3 and 19.2.4. Sibling of #23316.

Summary

This PR can be summarized in the following changelog entry:

  • [ui-library] Replaces deprecated defaultProps with ES6 default parameters across @yoast/ui-library components so they keep rendering on React 19, which ignores defaultProps for function components.

Relevant technical choices:

The PR has three commits:

  1. defaultProps → ES6 defaults across 33 components. The 12 the conversion pushes over the ESLint complexity warning get a file-level eslint-disable complexity (matching the existing ~118-file convention in this repo; the threshold itself is unchanged). Tooltip's variant was previously forwarded via ...props; it is re-forwarded explicitly so the change stays behaviour-neutral.
  2. Stabilise empty object/array defaults. defaultProps provided one stable instance; inline {} / [] defaults allocate a fresh value each render, breaking dependency-array identity. Left as-is this regressed three components (Stepper's steps looped a layout effect, Select's options fed a useMemo, TagInput's tags fed a useCallback). Hoisted every such default to a module-level constant.
  3. (separate commit, pre-existing) Stabilise the same pattern in CheckboxGroup, RadioGroup, and Root, which already used inline [] / {} defaults in trunk (never defaultProps-backed); CheckboxGroup's values feeds a useCallback dependency. Found while auditing the conversion, hence the separate commit.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

  • On the Gutenberg 23.3 RC (React 19.2.4), open a post with the browser console open: the Yoast metabox/sidebar render with no errors (without this change the metabox fails to render).
  • On current WordPress (React 18.3), confirm no visual regression in @yoast/ui-library UI (Settings, metabox, buttons, fields, modals).

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

The key check is the browser console in the block editor on a React 19 build: the Yoast metabox and sidebar must render.

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

QA tests on the Gutenberg 23.3-RC1 (React 19) build, following the acceptance test steps above.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • Anything rendering @yoast/ui-library components: the Yoast SEO Settings UI, the metabox/sidebar, and add-ons that consume the package.

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change.

Quality assurance

  • I have tested this code to the best of my abilities. Verified on a React 19.2.4 (Gutenberg 23.3-RC1) site (metabox, sidebar and Settings render cleanly) and via the follow-up render smoke test (58/58 components on the React 18.3 / 19.2.4 matrix).
  • During testing, I had activated all plugins that Yoast SEO provides integrations for. Tested with Free, Premium, Local, News and Video active.
  • I have added unit tests to verify the code works as intended. The render smoke test ships in the follow-up tests PR.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Refs Yoast/reserved-tasks#231, Yoast/reserved-tasks#246

Fixes Yoast/reserved-tasks#1250

🤖 Generated with Claude Code

@enricobattocchi enricobattocchi requested a review from a team as a code owner May 29, 2026 11:11
@enricobattocchi enricobattocchi added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label May 29, 2026
@coveralls
Copy link
Copy Markdown

coveralls commented May 29, 2026

Coverage Report for CI Build 0

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.01%) to 57.791%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 13 coverage regressions across 3 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

13 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
packages/js/src/general/app.js 9 0.0%
packages/js/src/general/components/notice.js 3 7.69%
packages/js/src/containers/PersistentDismissableNotification.js 1 15.38%

Coverage Stats

Coverage Status
Relevant Lines: 27075
Covered Lines: 16030
Line Coverage: 59.21%
Relevant Branches: 17138
Covered Branches: 9521
Branch Coverage: 55.55%
Branches in Coverage %: Yes
Coverage Strength: 111125.29 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR prepares @yoast/ui-library components for React 19 by replacing function/forwardRef component defaultProps with ES6 default parameters.

Changes:

  • Moves component defaults into parameter destructuring across UI library elements and components.
  • Removes defaultProps assignments from the converted components.
  • Adds complexity lint disables where the default-parameter conversion increases reported complexity.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/ui-library/src/elements/validation/validation-input.js Removes defaultProps after moving validation defaults.
packages/ui-library/src/elements/tooltip/index.js Moves tooltip defaults into destructuring.
packages/ui-library/src/elements/toggle/index.js Converts toggle defaults and updates lint directive.
packages/ui-library/src/elements/title/index.js Converts title defaults and removes no-undefined directive.
packages/ui-library/src/elements/textarea/index.js Converts textarea defaults.
packages/ui-library/src/elements/text-input/index.js Converts text input defaults.
packages/ui-library/src/elements/tag-input/index.js Converts tag input defaults and adds complexity disable.
packages/ui-library/src/elements/table/index.js Removes table defaultProps.
packages/ui-library/src/elements/spinner/index.js Converts spinner defaults.
packages/ui-library/src/elements/select/index.js Converts select defaults and adds complexity disable.
packages/ui-library/src/elements/radio/index.js Converts radio defaults and adds complexity disable.
packages/ui-library/src/elements/progress-bar/index.js Removes progress bar defaultProps.
packages/ui-library/src/elements/paper/index.js Removes paper defaultProps.
packages/ui-library/src/elements/link/index.js Converts link defaults.
packages/ui-library/src/elements/label/index.js Converts label defaults.
packages/ui-library/src/elements/file-input/index.js Converts file input defaults.
packages/ui-library/src/elements/code/index.js Removes code defaultProps.
packages/ui-library/src/elements/checkbox/index.js Removes checkbox defaultProps.
packages/ui-library/src/elements/button/index.js Converts button defaults and adds complexity disable.
packages/ui-library/src/elements/badge/index.js Converts badge defaults.
packages/ui-library/src/elements/autocomplete/index.js Converts autocomplete defaults and adds complexity disable.
packages/ui-library/src/elements/alert/index.js Removes alert defaultProps.
packages/ui-library/src/components/toggle-field/index.js Converts toggle field defaults and adds complexity disable.
packages/ui-library/src/components/textarea-field/index.js Converts textarea field defaults and adds complexity disable.
packages/ui-library/src/components/text-field/index.js Converts text field defaults and adds complexity disable.
packages/ui-library/src/components/tag-field/index.js Converts tag field defaults and adds complexity disable.
packages/ui-library/src/components/stepper/index.js Removes stepper defaultProps.
packages/ui-library/src/components/select-field/index.js Converts select field defaults and adds complexity disable.
packages/ui-library/src/components/modal/index.js Converts modal subcomponent defaults.
packages/ui-library/src/components/modal/container.js Converts modal container defaults.
packages/ui-library/src/components/file-import/index.js Converts file import defaults.
packages/ui-library/src/components/card/index.js Converts card defaults.
packages/ui-library/src/components/autocomplete-field/index.js Converts autocomplete field defaults and adds complexity disable.
Comments suppressed due to low confidence (1)

packages/ui-library/src/elements/tooltip/index.js:39

  • This conversion removes variant from the rest props, so it is no longer forwarded to the underlying Component. Before this change, variant stayed inside props and was spread onto Component, including for custom as components; to keep this behavior-neutral, pass it through explicitly when spreading the remaining props.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 33 out of 33 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 36 out of 36 changed files in this pull request and generated no new comments.

enricobattocchi and others added 3 commits May 29, 2026 20:24
…eact 19

React 19 ignores `defaultProps` on function/forwardRef components, so any
component that relied on them received `undefined` for those props and threw
when the value was dereferenced (e.g. Title's `as`, Button's `variant`),
crashing the whole React tree. This is what stopped the Yoast metabox from
rendering on the Gutenberg 23.3 RC (React 19.2.4).

Move every defaulted prop into ES6 default parameters across the ui-library
components that used `defaultProps` (33 files) and remove the now-unused
`.defaultProps` blocks. Behaviour is unchanged on React 18.

The 12 components the conversion pushed over ESLint's `complexity` warning
threshold (each ES6 default counts as a branch) get a file-level
`eslint-disable complexity`, matching the existing convention in this repo
(e.g. ui-library's components/notifications and ~118 other files).

Refs: Yoast/reserved-tasks#231, Yoast/reserved-tasks#246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…faults

`defaultProps` created one stable instance per object/array default, but the
ES6 conversion replaced them with inline `{}` / `[]` literals that allocate a
fresh value every render. Where such a default feeds a hook dependency array
this broke referential equality and re-fired effects in a loop:

- Stepper: `steps` is in a useLayoutEffect dependency list that calls
  setStepRefs, so rendering <Stepper> with children but no `steps` looped.
- Select: `options` feeds a useMemo dependency, recomputed every render.
- TagInput: `tags` feeds a useCallback dependency, reallocated every render.

Hoist every empty object/array default to a module-level constant (matching the
old defaultProps single instance). The remaining ones (validation/labelProps/
buttonProps) are not in dependency arrays or passed to memoized children today,
but are stabilized too for parity and future safety.

Refs: Yoast/reserved-tasks#231, Yoast/reserved-tasks#246

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…up, RadioGroup, Root

Not part of the defaultProps->ES6 conversion: these components already used
inline `[]` / `{}` defaults in trunk (never defaultProps-backed), so each
allocates a fresh value every render. CheckboxGroup's `values` feeds a
useCallback dependency, so a fresh `[]` reallocated the change handler on every
render; RadioGroup's `options` and Root's `context` are stabilized for parity
(not currently in dependency arrays). Hoist all three to module-level constants.

Found while auditing the React 19 defaultProps conversion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enricobattocchi enricobattocchi force-pushed the react19-ui-library-defaultprops branch from dff3f27 to c489731 Compare May 29, 2026 18:24
@enricobattocchi enricobattocchi changed the base branch from trunk to release/27.8 May 29, 2026 18:24
@enricobattocchi enricobattocchi changed the base branch from release/27.8 to feature/fix-gutenberg-23.3 May 29, 2026 21:30
@enricobattocchi enricobattocchi merged commit df47d0a into feature/fix-gutenberg-23.3 May 29, 2026
26 of 42 checks passed
@enricobattocchi enricobattocchi deleted the react19-ui-library-defaultprops branch May 29, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants