Skip to content

chore(deps): L3-0000 upgrade seldon dev deps to React 19 - #910

Open
scottdickerson wants to merge 8 commits into
mainfrom
chore/react-19-upgrade
Open

scottdickerson wants to merge 8 commits into
mainfrom
chore/react-19-upgrade

Conversation

@scottdickerson

Copy link
Copy Markdown
Contributor

Jira ticket

N/A — dependency housekeeping. Successor to #808 (which failed CI because it bumped react-dom without bumping react, breaking version alignment).

Screenshots

Before After
N/A — no runtime UI changes. Verify via Chromatic. Verify via Chromatic.

Figma link

N/A.

Summary

Upgrades seldon's dev-time React from 18.3.1 to 19.2.8 (latest stable) while preserving the peer-dependency contract that already supports both React 18 and 19. Downstream consumers of @phillips/seldon can stay on React 18 today and migrate to React 19 on their own schedule — this PR only changes what seldon builds and tests against locally / in CI.

WHY: React 19 has been stable since late 2024. Keeping seldon's dev dep on 18 means we never CI-test the library against 19, so any React-19-only regression (from a Radix update, a Storybook update, or our own code) would leak to consumers before we catch it. Also unblocks downstream phillips-public-remix if/when it moves to 19.

Change List (describe the changes made to the files)

Dependency changes (package.json + package-lock.json):

Bumped in devDependencies:

  • react 18.3.1 → 19.2.8
  • react-dom 18.2.0 → 19.2.8
  • @types/react 18.3.3 → 19.2.17
  • @types/react-dom 18.0.11 → 19.2.3
  • @testing-library/react 16.0.0 → 16.3.2 (peer now ^18.0.0 || ^19.0.0)
  • usehooks-ts 3.1.0 → 3.1.1 (peer now ^18 || ^19)
  • @chromatic-com/storybook 5.0.1 → 5.2.1 (Storybook 10.6.x peer support)

Added:

  • @testing-library/dom ^10.4.1 — now a required peer of @testing-library/react 16.x

Not changed: peerDependencies block still declares react: ^18.3.1 || ^19.0.0 and react-dom: ^18.2.0 || ^19.0.0 — the "consumers can pick either" contract from #808's era is preserved.

Code changes to accommodate React 19's stricter types + removed APIs:

RefObject types — React 19 useRef<T>(null) returns RefObject<T | null> (previously RefObject<T>). Prop types widened accordingly:

  • CarouselDot.tsx (scrollableContainerRef)
  • CountryPickerCountryList.tsx (listRef)
  • FavoritesCollectionTile.tsx (imageRef)
  • ComboBox.tsx (widened prop + cast for usehooks-ts useOnClickOutside)
  • Cast at useOnClickOutside call sites (Search.tsx, ComboBox.tsx) — usehooks-ts still types the parameter as RefObject<T> and doesn't accept the nullable form.

JSX namespace — React 19 removed the global JSX namespace. Switched to React.JSX.Element or added import type { JSX } from 'react':

  • utils/index.tsx, DetailList.stories.tsx, ProgressWizard.stories.tsx

Children / cloneElement — React 19 types React.Children.map callback param as ReactElement<unknown>. Added explicit generics on React.isValidElement<Props>() and typed cloneElement calls:

  • DetailList.tsx, NavigationSubmenu.tsx, Toast.test.tsx, FilterMenu.tsx, ProgressWizard/utils.tsx

DatePicker.tsxuseRef() now requires an initial value. Also removed extends Record<string, unknown> from DatePickerProps (a latent bug that was silently marking every declared prop unknown). Made locale optional to match its = 'en' default; typed the forwardRef signature explicitly.

StatefulViewingsList.tsx — same latent extends Record<string, unknown> bug removed; replaced with extends Omit<ViewingsListProps, ...> so the spread of forwarded props remains legal.

Search.tsxreact-transition-group@4.4.5's <CSSTransition> calls the removed ReactDOM.findDOMNode internally. Added nodeRef={searchInputRef} so the transition finds the DOM node via ref instead. (No consumer changes needed — this is our only usage.)

SeldonImage.tsx — the workaround fetchpriority (lowercase HTML attr, with @ts-expect-error + eslint-disable) is no longer needed; React 19 handles fetchPriority (camelCase) natively. Also guarded empty src with src || undefined to satisfy React 19's stricter empty-string warning.

Button.test.tsx — React 19 hoists <link> elements to <head> for de-duplication. Prefetch-link assertions now query document.head directly.

Acceptance Test (how to verify the PR)

  1. CI on this PR passes (build + lint + unit tests + Chromatic).
  2. Review Chromatic diff — no visual regressions expected. If any appear, they're most likely from React 19's stricter DOM warnings (empty src, fetchpriority casing) which have been addressed at the source.
  3. Bump @phillips/seldon in phillips-public-remix to a preview build of this branch and confirm the app still boots on React 18 (the consumer's current React version).
  4. Optional stretch: try bumping phillips-public-remix itself to React 19 to validate the peer-dep contract end-to-end.

Regression Test

  • Verify <Search> expand/collapse animation still works (nodeRef change on CSSTransition).
  • Verify <Toast> still renders (uses cloneElement with typed generics now).
  • Verify <DatePicker> opens and picks a date (useRef initialization + forwardRef typing changed).
  • Verify <SeldonImage> renders with fetchPriority="high" — DOM attribute should still be fetchpriority="high" (HTML is case-insensitive), just no more console warning.
  • Verify the <CountryPicker> and <FavoritesCollectionTile> refs still forward correctly to their internal list containers.

Evidence of testing

Local:

  • npm run build → exit 0 (tsc + vite build clean)
  • npm run lint → exit 0 (tsc --noEmit, eslint, stylelint, markdownlint)
  • npx vitest run --project=unit1042 / 1042 passing on React 19
  • Pre-push storybook tests → 238 / 238 passing (via husky hook)

Prior state on this branch had 83 test failures on React 19 — all fixed. The failures fell into predictable buckets: findDOMNode-removed (fixed via nodeRef), fetchpriority DOM warning (fixed via camelCase), empty-src warning (fixed via || undefined), and hoisted <link> in tests (fixed by querying document.head).

Known caveats — please spot-check

  • --legacy-peer-deps was used at install time to resolve a Storybook 10.3.3 ↔ @storybook/react 10.5.5 peer mismatch that also exists on main today. The lockfile is consistent; the flag doesn't leave anything broken.
  • react-transition-group remains at 4.4.5 (no React-19-compatible stable release exists). Our single usage in Search.tsx is patched via nodeRef — the standard workaround.
  • @types/react 18 → 19 is a moderately breaking API surface change for consumers writing generic React components. phillips-public-remix doesn't use these types directly.

Things to look for during review

  • PR title should correctly describe the most significant type of commit. I.e. feat(scope): ... if a minor release should be triggered.
  • All commit messages follow convention and are appropriate for the changes
  • All references to phillips class prefix are using the prefix variable
  • All major areas have a data-testid attribute.
  • Document all props with jsdoc comments
  • All strings should be translatable.
  • Unit tests should be written and should have a coverage of 90% or higher in all areas.

🤖 Generated with Claude Code

Bumps the dev dependency on React from 18.3.1 to 19.2.8 while
preserving the peer-dependency contract that already allowed both
React 18 and React 19 consumers. Also brings along the React
ecosystem packages whose peer types explicitly excluded React 19.

Superseded by attempt #808 (react-dom bumped without react — inconsistent).

## Dependency changes

Dev dependencies bumped:
- react 18.3.1 → 19.2.8
- react-dom 18.2.0 → 19.2.8
- @types/react 18.3.3 → 19.2.17
- @types/react-dom 18.0.11 → 19.2.3
- @testing-library/react 16.0.0 → 16.3.2 (React 19 peer support)
- usehooks-ts 3.1.0 → 3.1.1 (React 19 peer support)
- @chromatic-com/storybook 5.0.1 → 5.2.1 (Storybook 10.6.x peer support)

Added:
- @testing-library/dom ^10.4.1 (peer requirement of @testing-library/react 16.x)

peerDependencies unchanged — still declares
`"react": "^18.3.1 || ^19.0.0"` and `"react-dom": "^18.2.0 || ^19.0.0"`,
so consumers can stay on 18 or move to 19.

## Code changes for React 19 compat

RefObject prop types widened from `RefObject<T>` to `RefObject<T | null>`
(React 19 `useRef<T>(null)` now returns the narrower nullable type):
- CarouselDot.tsx (`scrollableContainerRef`)
- CountryPickerCountryList.tsx (`listRef`)
- FavoritesCollectionTile.tsx (`imageRef`)
- ComboBox.tsx (widened prop + cast for usehooks-ts `useOnClickOutside`)

`useOnClickOutside` cast at call site (usehooks-ts still types as
`RefObject<T>`) — Search.tsx, ComboBox.tsx.

JSX namespace no longer global in React 19; switched to `React.JSX.Element`
or added `import type { JSX }` — utils/index.tsx,
DetailList.stories.tsx, ProgressWizard.stories.tsx.

`React.Children` / `cloneElement` stricter — added explicit generics on
`isValidElement<Props>()` and typed cloneElement calls:
- DetailList.tsx, NavigationSubmenu.tsx, Toast.test.tsx,
  FilterMenu.tsx, ProgressWizard/utils.tsx.

DatePicker.tsx — `useRef()` now requires an initial value; added
`null`/`undefined`. Also removed `extends Record<string, unknown>`
from DatePickerProps (was silently marking every prop `unknown`),
typed forwardRef explicitly, made `locale` optional to match its
`= 'en'` default.

StatefulViewingsList.tsx — removed the same latent
`extends Record<string, unknown>` bug; replaced with
`extends Omit<ViewingsListProps, ...>` so the spread of forwarded
props remains legal.

Search.tsx — added `nodeRef={searchInputRef}` to `<CSSTransition>` so
react-transition-group no longer calls the removed
`ReactDOM.findDOMNode`.

SeldonImage.tsx — switched the workaround `fetchpriority` (lowercase
HTML attr) back to the proper camelCase `fetchPriority` which React
19 now handles natively; removed the accompanying `@ts-expect-error`
and eslint-disable. Also guarded empty `src` with `src || undefined`
to satisfy React 19's stricter empty-string warning.

Button.test.tsx — React 19 hoists `<link>` elements to `<head>`, so
the prefetch-link assertions now query `document.head`.

## Verification

- npm run build → exit 0 (tsc + vite build)
- npm run lint → exit 0 (tsc --noEmit, eslint, stylelint, markdownlint)
- npx vitest run --project=unit → 1042 / 1042 passing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 19:19
@netlify

netlify Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploy Preview for phillips-seldon ready!

Name Link
🔨 Latest commit 13aefc9
🔍 Latest deploy log https://app.netlify.com/projects/phillips-seldon/deploys/6a67ba67690bef0008a73845
😎 Deploy Preview https://deploy-preview-910--phillips-seldon.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@chromatic-com

chromatic-com Bot commented Jul 27, 2026

Copy link
Copy Markdown

Tip

All tests passed and all changes approved!

🟢 UI Tests: 476 tests unchanged
🟢 UI Review: 238 stories published -- no changes
Storybook icon Storybook Publish: 238 stories published

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 upgrades Seldon’s dev-time React toolchain to React 19 (while keeping the existing peer-dependency contract supporting both React 18 and 19), and updates component/test typings and a few implementation details to align with React 19’s stricter types and removed APIs.

Changes:

  • Bump devDependencies to React 19 (plus aligned types/testing deps) and update lockfile accordingly.
  • Update TypeScript types across components/stories/tests for React 19 changes (refs now nullable, JSX namespace changes, stricter Children/cloneElement typing).
  • Patch the only react-transition-group usage by providing nodeRef, and update DOM/test behavior changes (e.g., fetchPriority, <link> hoisting).

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utils/index.tsx Switches internal types away from global JSX to React.JSX for React 19 compatibility.
src/patterns/ViewingsList/StatefulViewingsList.tsx Fixes props typing by extending Omit<ViewingsListProps, ...> to avoid the previous Record<string, unknown> typing issue.
src/patterns/ProgressWizard/utils.tsx Tightens isValidElement generic typing to satisfy React 19 Children element typing.
src/patterns/ProgressWizard/ProgressWizard.stories.tsx Imports JSX type explicitly for React 19 and uses it in story typing.
src/patterns/FilterMenu/FilterMenu.tsx Adds explicit generic to isValidElement to keep cloneElement strongly typed under React 19.
src/patterns/FavoritesCollectionTile/FavoritesCollectionTile.tsx Widens ref prop types to accept `RefObject<T
src/patterns/CountryPicker/CountryPickerCountryList.tsx Widens ref prop types to accept `RefObject<T
src/components/Toast/Toast.test.tsx Updates isValidElement/cloneElement typing in mocks to satisfy React 19 element typing.
src/components/SeldonImage/SeldonImage.tsx Uses fetchPriority (React 19 native support) and avoids empty-string src warnings via `src
src/components/Search/Search.tsx Avoids findDOMNode via CSSTransition nodeRef and casts refs for useOnClickOutside typing.
src/components/Navigation/NavigationSubmenu/NavigationSubmenu.tsx Updates isValidElement/cloneElement generics and event typing for React 19 element narrowing.
src/components/DetailList/DetailList.tsx Adds explicit isValidElement generic to keep child props typed under React 19.
src/components/DetailList/DetailList.stories.tsx Imports JSX type explicitly for React 19 (no global namespace).
src/components/DatePicker/DatePicker.tsx Fixes React 19 useRef initialization requirements and corrects the props typing regression from Record<string, unknown>.
src/components/DatePicker/DatePicker.stories.tsx Updates useRef initialization for React 19.
src/components/ComboBox/ComboBox.tsx Widens nullable ref prop type and casts for useOnClickOutside typing under React 19.
src/components/Carousel/CarouselDot.tsx Widens ref prop types to accept `RefObject<T
src/components/Carousel/CarouselDot.test.tsx Updates test ref typing to match updated CarouselDot prop types.
src/components/Button/Button.test.tsx Adjusts assertions to account for React 19 <link> hoisting into document.head.
package.json Bumps React/ReactDOM/types/testing/chromatic devDependencies and adds @testing-library/dom.
package-lock.json Updates resolved dependency tree for the React 19 and related dev dependency upgrades.

Comment thread src/patterns/ViewingsList/StatefulViewingsList.tsx Outdated
Comment thread src/components/DatePicker/DatePicker.tsx
Comment thread src/components/DatePicker/DatePicker.tsx Outdated
scottdickerson and others added 7 commits July 27, 2026 14:34
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Two behavior-preserving fixes flagged by code review:

1. SeldonImage — empty src no longer silently skips the error UI.
   Previously `<img src="">` fired the browser onError event which
   set loadingState to 'error' and showed the error icon. With
   `src={src || undefined}` (to silence React 19's empty-src warning)
   the error path never triggered. Now initialize loadingState to
   'error' when !src, and short-circuit the async loadImage so it
   doesn't fire a stray setState after unmount.

2. Search — CSSTransition classes now land on the wrapper div, not
   the inner <input>. React-transition-group's findDOMNode used to
   return Input's outermost DOM node (the wrapper <div>) so
   `.seldon-input-enter` classes were applied there. Switching to
   `nodeRef={searchInputRef}` shifted them to the input element
   itself, a silent DOM-target change for any downstream consumer
   styling those transition classes. Added a dedicated
   searchInputWrapperRef and a wrapping div so nodeRef targets the
   wrapper.

Verified: build passes, lint passes, 1042/1042 unit tests pass, no
unhandled rejections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior wrapper-div fix around <CSSTransition> introduced an extra
block that disrupted the flex chain, pushing the input to a new row.
Removes the wrapper and lands the transition classes on the <input>
directly, then updates the SCSS selector from
`.seldon-input-enter-active input` (descendant) to
`.seldon-input__input.seldon-input-enter-active` so the enter-active
background/placeholder styling still applies.

Also:
- Drops stale explanatory comments across Search, SeldonImage, and
  the Button test helper that documented the React-18→19 delta
  rather than the code's current behavior.
- Adds .claude/worktrees to eslint ignores so lint doesn't fail on
  files that belong to a locked git worktree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prevents `.claude/worktrees/` from being accidentally added as a
submodule entry when the pre-commit hook runs `git add .`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the eslint-config change so `npm run coverage` and the
pre-push hook don't pick up test files in a locally-checked-out
worktree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottdickerson
scottdickerson enabled auto-merge (squash) July 27, 2026 20:10
@adietrich3074

Copy link
Copy Markdown
Contributor

🤖 This comment was generated by an AI agent (thermo-nuclear code quality review).

Outstanding fixes (type-boundary cleanliness):

  1. Duplicate unsound cast for useOnClickOutsideComboBox.tsx:361 and Search.tsx:96 both cast a RefObject<T | null> to RefObject<T> to satisfy usehooks-ts's stale pre-React-19 signature. Extract a single shared wrapper (e.g. useOnClickOutsideRef) that owns the cast once with a comment, instead of repeating an unexplained cast at each call site.

  2. DatePicker.tsx:154const fp = React.useRef<flatpickr.Instance>(null!) asserts non-null, but every access in the file (fp?.current?.x, if (fp.current && ...)) already treats it as nullable. Use useRef<flatpickr.Instance | null>(null) instead — no behavior change, just an honest type that matches how the ref is actually used.

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