feat: support React 19#1074
Conversation
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>
doistbot
left a comment
There was a problem hiding this comment.
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
validatepath from one Jest run to two full runs. Becausesetupshells throughvalidate, 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.
| 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. |
There was a problem hiding this comment.
🟠 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.
There was a problem hiding this comment.
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.
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>
doistbot
left a comment
There was a problem hiding this comment.
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/react19 and 18 are installed, ambient React 19 declarations can still leak in. Narrowingtypes/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
LegacyRefstrings, so JS or class-component consumers could now pass a string ref that gets silently forwarded intoTooltipAnchorinstead of getting a clear unsupported-usage error — worth re-adding the guard after resolvingchildRef. - Expanding
validate(and by extensionsetup) 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:
validateis the project's default local verification path, andsetupshells through it. Expanding it totype-check + type-check:react18 + test + test:react18means 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.
| "@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"], |
There was a problem hiding this comment.
🟡 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.
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>

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
useActionStatetype-checknpm scripttype-check:react18npm scriptPR Checklist