Skip to content

feat: support React 19#1074

Draft
frankieyan wants to merge 5 commits into
mainfrom
frankie/react-19-compat
Draft

feat: support React 19#1074
frankieyan wants to merge 5 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 will be run using both 18 and 19.

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

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
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.

Comment thread package.json
"postinstall": "patch-package",
"setup": "npm install && npm run validate",
"validate": "npm run lint && npm run type-check && npm run test",
"validate": "npm run lint && npm run type-check && npm run test && npm run test:react18",

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 adds a second runtime test pass, but validate still runs only one type-check against the React 19 typings from devDependencies. Because this package publishes declarations and now claims React 18 + 19 support, we should also add a React 18 typings/declaration check here; otherwise React 18-only type regressions can slip through CI.

Comment thread src/utils/polymorphism.ts
ShouldObfuscateClassName
>
return React.forwardRef(
render as PolymorphicRenderFunction<ComponentType, OwnProps, ShouldObfuscateClassName>,

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 cast papers over a real contract mismatch in the polymorphic helper. PolymorphicComponentProps is still built from React.ComponentProps, which includes ref under React 19, but a forwardRef render never receives props.ref. With the cast in place, future components can type-check while depending on a prop React strips at runtime. Please move the PropsWithoutRef/ForwardedRef change into the helper types themselves instead of masking it here.

</g>
</svg>
)
ThreeDotsIcon.propTypes = {

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 Removing propTypes here also removes the only prop contract this TSX component had. Since the function parameter is still untyped, color falls back to any, so consumers lose both runtime validation and compile-time checking. Please add a typed props annotation/interface before dropping propTypes.

run: |
npm run test
- name: Test codebase correctness (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 adds a second full Jest pass in the same job, so every PR now waits for the React 19 and React 18 suites serially. These runs are independent; a matrix or separate jobs would keep wall-clock time close to one test run while still checking both runtimes.

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.

2 participants