From b301aa2bca017b40a4c68aa98b6c7b7c1f09d6ac Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 7 Mar 2026 16:01:56 +0100 Subject: [PATCH 1/4] Docs: add controlled/uncontrolled prop naming conventions to @wordpress/ui README Document the `defaultX` / `x` / `onXChange` prop naming pattern used across interactive components in the package. Covers uncontrolled vs controlled modes, difference from native `onChange`, and guidelines for component authors. Ref: https://github.com/WordPress/gutenberg/pull/76252#discussion_r2896825451 Made-with: Cursor --- packages/ui/README.md | 106 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/packages/ui/README.md b/packages/ui/README.md index f22f7a3a076c02..caa891f2c3ba00 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -96,6 +96,112 @@ function MyComponent() { } ``` +### Controlled and Uncontrolled Modes + +Interactive components that manage internal state (such as open/closed, selected value, etc.) follow a consistent prop naming convention that supports both **controlled** and **uncontrolled** usage. + +#### Prop naming pattern + +For a given state `x`, the convention is: + +| Prop | Purpose | +| --- | --- | +| `defaultX` | Sets the initial value in **uncontrolled** mode. The component manages subsequent state changes internally. | +| `x` | Sets the current value in **controlled** mode. The consumer is responsible for updating the value in response to changes. | +| `onXChange` | Callback invoked when the state changes. Receives the new value as an argument. Works in both controlled and uncontrolled modes. | + +For example, a component with an open/closed state would expose: + +- `defaultOpen` — initial open state (uncontrolled) +- `open` — current open state (controlled) +- `onOpenChange` — called when the open state changes + +And a component with a selectable value would expose: + +- `defaultValue` — initial value (uncontrolled) +- `value` — current value (controlled) +- `onValueChange` — called when the value changes + +#### Uncontrolled usage + +In uncontrolled mode, the component manages its own state. Use `defaultX` to set the initial value, and optionally `onXChange` to react to changes: + +```tsx +import { Tabs } from '@wordpress/ui'; + +function MyTabs() { + return ( + console.log( value ) } + > + + Tab 1 + Tab 2 + + Content 1 + Content 2 + + ); +} +``` + +#### Controlled usage + +In controlled mode, the consumer owns the state and passes it via `x`. State changes are handled through `onXChange`: + +```tsx +import { useState } from 'react'; +import { CollapsibleCard } from '@wordpress/ui'; + +function MyCard() { + const [ isOpen, setIsOpen ] = useState( false ); + + return ( + + Details + + Collapsible content here. + + + ); +} +``` + +When both `x` and `defaultX` are provided, `x` takes precedence and the component behaves in controlled mode. + +#### Difference from native `onChange` + +The `onXChange` callback is distinct from the native DOM `onChange` event handler. Native `onChange` fires a `React.ChangeEvent` tied to a specific DOM element, while `onXChange` provides the new **value** directly — making it simpler to use and consistent across all components, including compound and non-form components. + +Components that wrap native form elements may still support native event handlers (like `onChange`, `onInput`) for interoperability, but `onXChange` is the recommended approach within this package. + +#### Guidelines for component authors + +When designing props for a new component: + +- Always offer both controlled and uncontrolled modes when the component has user-facing state. +- Name the uncontrolled prop `defaultX`, the controlled prop `x`, and the callback `onXChange`. +- In JSDoc comments, indicate which mode each prop is for and cross-reference the alternative: + ```ts + /** + * Whether the panel is currently open (controlled). + * + * To render an uncontrolled component, use the `defaultOpen` prop instead. + */ + open?: boolean; + /** + * Whether the panel is initially open (uncontrolled). + * @default false + */ + defaultOpen?: boolean; + /** + * Event handler called when the open state changes. + */ + onOpenChange?: ( open: boolean ) => void; + ``` +- Provide a `@default` JSDoc tag for the uncontrolled prop when there is a sensible default. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. From e061ff2509a32dde8461bfd7c90eb6a012a478a6 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 7 Mar 2026 16:02:51 +0100 Subject: [PATCH 2/4] Docs: cross-reference controlled/uncontrolled conventions from Tabs best practices Add a note at the top of the Tabs best practices page linking to the package-wide controlled/uncontrolled prop naming conventions documented in the README. Made-with: Cursor --- packages/ui/src/tabs/stories/best-practices.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/src/tabs/stories/best-practices.mdx b/packages/ui/src/tabs/stories/best-practices.mdx index 41e7b157e3534e..f146e3ff494238 100644 --- a/packages/ui/src/tabs/stories/best-practices.mdx +++ b/packages/ui/src/tabs/stories/best-practices.mdx @@ -6,6 +6,8 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Usage +The `Tabs` component supports both controlled and uncontrolled modes following the [package-wide conventions](https://github.com/WordPress/gutenberg/blob/HEAD/packages/ui/README.md#controlled-and-uncontrolled-modes) (`defaultValue` / `value` / `onValueChange`). + ### Uncontrolled Mode `Tabs` can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initially selected tab. From c838f3e3f2f0de290b78a7b35315376c5e7bee90 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 7 Mar 2026 16:09:50 +0100 Subject: [PATCH 3/4] Docs: address review feedback on controlled/uncontrolled conventions - Use `@wordpress/element` import instead of `react` for consistency with the rest of the README and package source - Clarify that `onXChange` receives the new value as its "first" argument (some Base UI-derived components pass additional arguments) - Document behavior when neither `x` nor `defaultX` is provided Made-with: Cursor --- packages/ui/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/README.md b/packages/ui/README.md index caa891f2c3ba00..d902e18560e75c 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -108,7 +108,7 @@ For a given state `x`, the convention is: | --- | --- | | `defaultX` | Sets the initial value in **uncontrolled** mode. The component manages subsequent state changes internally. | | `x` | Sets the current value in **controlled** mode. The consumer is responsible for updating the value in response to changes. | -| `onXChange` | Callback invoked when the state changes. Receives the new value as an argument. Works in both controlled and uncontrolled modes. | +| `onXChange` | Callback invoked when the state changes. Receives the new value as its first argument. Works in both controlled and uncontrolled modes. | For example, a component with an open/closed state would expose: @@ -151,7 +151,7 @@ function MyTabs() { In controlled mode, the consumer owns the state and passes it via `x`. State changes are handled through `onXChange`: ```tsx -import { useState } from 'react'; +import { useState } from '@wordpress/element'; import { CollapsibleCard } from '@wordpress/ui'; function MyCard() { @@ -168,7 +168,7 @@ function MyCard() { } ``` -When both `x` and `defaultX` are provided, `x` takes precedence and the component behaves in controlled mode. +When both `x` and `defaultX` are provided, `x` takes precedence and the component behaves in controlled mode. When neither is provided, the component uses its own internal default (typically documented via a `@default` JSDoc tag on the `defaultX` prop). #### Difference from native `onChange` From b918286d2032da07699c1c5b31563945293c3018 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sun, 8 Mar 2026 12:09:58 +0100 Subject: [PATCH 4/4] Revert "Docs: cross-reference controlled/uncontrolled conventions from Tabs best practices" This reverts commit e061ff2509a32dde8461bfd7c90eb6a012a478a6. --- packages/ui/src/tabs/stories/best-practices.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ui/src/tabs/stories/best-practices.mdx b/packages/ui/src/tabs/stories/best-practices.mdx index f146e3ff494238..41e7b157e3534e 100644 --- a/packages/ui/src/tabs/stories/best-practices.mdx +++ b/packages/ui/src/tabs/stories/best-practices.mdx @@ -6,8 +6,6 @@ import { Meta } from '@storybook/addon-docs/blocks'; ## Usage -The `Tabs` component supports both controlled and uncontrolled modes following the [package-wide conventions](https://github.com/WordPress/gutenberg/blob/HEAD/packages/ui/README.md#controlled-and-uncontrolled-modes) (`defaultValue` / `value` / `onValueChange`). - ### Uncontrolled Mode `Tabs` can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initially selected tab.