Skip to content

feat: support React 19#1074

Open
frankieyan wants to merge 10 commits into
mainfrom
frankie/react-19-compat
Open

feat: support React 19#1074
frankieyan wants to merge 10 commits into
mainfrom
frankie/react-19-compat

Conversation

@frankieyan

@frankieyan frankieyan commented Jun 16, 2026

Copy link
Copy Markdown
Member

Short description

This PR adds React 19 support. Storybook/Chromatic now runs v19 but tests and type checks will be run using both 18 and 19.

We attempted to run Storybook in v18 in parallel as well, but no luck so far.

Test plan

  • Introduce a React 19 only API, such as useActionState
  • It passes the normal type-check npm script
  • It fails the type-check:react18 npm script

PR Checklist

  • Added tests for bugs / new features
  • Updated docs (storybooks, readme)
  • Reviewed and approved Chromatic visual regression tests in CI

frankieyan and others added 5 commits June 15, 2026 23:37
Replace ReactDOM.findDOMNode(this) in the deprecated ColorPicker dropdown
Box with a typed root ref (removed in React 19), and delete the redundant
Button.defaultProps block (ignored for forwardRef in React 19; already
shadowed by the destructuring defaults). Both fixes are version-safe under
React 18.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump dev react/react-dom/@types/react/@types/react-dom/react-is to 19.x and
widen the peer ranges to >=18 <20 (additive for existing React 18 consumers).
Run types-react-codemod preset-19 for the React.JSX scoping and useRef initial
argument, then hand-fix the React 19 type and runtime breakages:

- tooltip: read the child ref from props.ref under React 19 (falls back to
  element.ref on 18) instead of the removed element.ref.
- polymorphism: cast the forwardRef render fn to React 19's PropsWithoutRef
  render signature.
- stack: widen react-keyed-flatten-children's React 19-removed ReactChild[]
  return to ReactNode[].
- revert the codemod's ReactElement<any> additions (the repo bans explicit any;
  bare ReactElement now defaults to unknown).
- add @types/prop-types, previously pulled in transitively by @types/react 18.

Also drive the hover-opened submenu test selection via keyboard: jsdom has no
layout, so ariakit closes the submenu when the pointer moves onto an item, and
React 19's synchronous flushing unmounts it before the click lands. Real-browser
hover + click works (verified in Storybook); keyboard selection is layout-
independent and version-stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add aliased react-18 / react-dom-18 dev dependencies and a second Jest config
that remaps react/react-dom to them, so the suite runs against React 18 from a
single install. Wire test:react18 into validate (and therefore the pre-push
hook), sharing the same committed snapshots.

react-dom-18 peers on react ^18 while the top-level react is 19; an overrides
entry points its react peer at the top-level version so npm resolves the tree
(Jest remaps everything to the 18 aliases at run time regardless).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a second unit-testing step running npm run test:react18 off the same
install, so CI exercises the suite under both React versions. Two sequential
steps on the single runner; a strategy matrix is unnecessary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
React 19 ignores propTypes, so drop the only two uses: the import + propTypes
block in ThreeDotsIcon and the propTypes member in the deprecated polymorphism
types. Remove prop-types from peerDependencies and the @types/prop-types dev
dependency (added in the React 19 bump only to type the now-deleted usage).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frankieyan frankieyan requested a review from doistbot June 16, 2026 08:06

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR updates feat: support React 19.

Few things worth tightening:

  • See the inline comments for the issues found in the review.

I also included a few optional follow-up notes in the details below.

Optional follow-up note (1)
  • [P3] package.json:41: This changes the default validate path from one Jest run to two full runs. Because setup shells through validate, fresh local installs now pay that extra pass too. If the React 18 suite only needs broad coverage in CI, move it to a dedicated full/CI script instead of doubling the common local check.

Share FeedbackReview Logs

Comment thread src/menu/menu.test.tsx Outdated
expect(screen.getByRole('menuitem', { name: 'Save' })).toBeVisible()
})

// Select via keyboard: in React 19 the hover-opened submenu closes synchronously when the pointer moves onto an item, unmounting it before a click can land.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟠 P1 This change papers over a real React 19 regression instead of covering it. The new comment says a hover-opened submenu closes as soon as the pointer moves onto an item, which means mouse users can't click submenu actions like "Save" anymore. Since this PR expands support to React 19, please fix that interaction and keep this test exercising the click path rather than switching it to keyboard-only selection.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This only happens because jsdom doesn't provide proper DOMRects, and Ariakit triggers a hide because of it: https://github.com/ariakit/ariakit/blob/200aa56676b8373fe10ec6c5fac61003ae82c1de/packages/ariakit-react-core/src/hovercard/hovercard.tsx#L201-L222. In React 18, the test had a short window for the click event to be handled, and with React 19, this queued render happens earlier, eliminating this window.

Claude found react/react#26512 after bisecting between different React commits and running this test, and this new behaviour is more correct.to me We could mock the DOMRects for Ariakit to pick up, but that feels like it's testing too much into Ariakit's internals, and I'd rather stick with the keyboard flow.

Comment thread package.json Outdated
Comment thread src/utils/polymorphism.ts Outdated
Comment thread src/components/icons/ThreeDotsIcon.svg.tsx
Comment thread .github/workflows/check-ci-validation.yml
frankieyan and others added 4 commits June 16, 2026 13:17
PolymorphicComponentProps is built on React.ComponentProps, which includes
ref under React 19. A forwardRef render never receives ref in props, so move
the strip into the ForwardRefFunction contract via PropsWithoutRef and
ForwardedRef, dropping the cast and the now-unused PolymorphicRenderFunction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract a named ThreeDotsIconProps type and switch to a function declaration
so the color prop is explicitly typed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explain why the submenu test drives selection via keyboard: jsdom can't give
Ariakit the DOMRects it needs to keep the hovercard open, and React 19 flushes
the close render before a click can land.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tsconfig.react18.json that remaps react/react-dom to aliased @types/react@18
and @types/react-dom@18, wired into a type-check:react18 script, the validate
chain, and CI. This catches typings regressions against the older React the
library still supports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frankieyan frankieyan marked this pull request as ready for review June 17, 2026 04:24

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR adds React 19 support across the library, with Storybook/Chromatic running on v19 and tests/type-checks running against both 18 and 19.

Few things worth tightening:

  • The React 18 type-check config only remaps module paths via paths, but since both @types/react 19 and 18 are installed, ambient React 19 declarations can still leak in. Narrowing types/typeRoots (or otherwise isolating the packages) would make the React 18 check actually catch v19-only type usage.
  • The Tooltip ref fallback removed the previous fast-fail for string refs. React 18 still allows LegacyRef strings, so JS or class-component consumers could now pass a string ref that gets silently forwarded into TooltipAnchor instead of getting a clear unsupported-usage error — worth re-adding the guard after resolving childRef.
  • Expanding validate (and by extension setup) to run both test and type-check matrices serially doubles the cost of every fresh install and routine local check. Consider keeping the dual-matrix run in a dedicated CI script rather than the default local path.

I also included a few optional follow-up notes in the details below.

Optional follow-up note (1)
  • [P3] package.json:41: validate is the project's default local verification path, and setup shells through it. Expanding it to type-check + type-check:react18 + test + test:react18 means every fresh install and routine local validation now pays two extra full passes. If the React 18 matrix is mainly for compatibility coverage, keep it in a dedicated CI/full-validation script instead of doubling the common local path.

Share FeedbackReview Logs

Comment thread tsconfig.react18.json
"@storybook/react-vite": ["node_modules/@storybook/react-vite/dist/index.d.ts"],
"storybook/actions": ["node_modules/storybook/dist/actions/index.d.ts"],
"storybook/test": ["node_modules/storybook/dist/test/index.d.ts"],
"react": ["node_modules/@types/react-18"],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 P2 This only remaps module imports. Because this repo now installs both @types/react 19 and @types/react-18, the React 18 compile can still pick up the 19 ambient declarations unless this config also narrows types/typeRoots. In that mixed setup, npm run type-check:react18 can still let React 19-only type usage slip through. Please isolate the React 18 config from the default @types/react* packages instead of relying on paths alone.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

React's types should be module scoped so I don't think this will be worth it. As it is, it should warn us when we are using React 19-only APIs:

image

Comment thread src/tooltip/tooltip.tsx Outdated
Throw on string refs instead of silently forwarding them, so React 18 consumers get a clear unsupported-usage error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@frankieyan frankieyan requested review from a team and rfgamaral and removed request for a team June 18, 2026 00:15

@rfgamaral rfgamaral left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good to me 🚀

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