feat(frontend): migrate to React 19 via Module Federation bridge#2525
Conversation
|
Cloud UI companion PR (consumes |
|
@claude review |
|
Claude finished @malinskibeniamin's task in 3m 40s —— View job PR Review: React 19 migration via Module Federation bridge
Reviewed the full diff. This is a careful, well-documented migration — the bridge approach is correct, the legacy shim is the right safety net, the dependency swaps ( Below are the things worth a second look before merge. 🔴 Other raw-component exposes have the same React-major hazard as
|
| * by the Apache License, Version 2.0 | ||
| */ | ||
|
|
||
| import { DragDropContext, Draggable, Droppable, type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; |
There was a problem hiding this comment.
react-beautiful-dnd is unmaintained and breaks under React 19: it pulls in react-redux@7, which calls ReactDOM.unstable_batchedUpdates at module init — that API was removed in React 19, so any drag-and-drop mount would throw at runtime. @hello-pangea/dnd is the maintained drop-in fork with an identical API (DragDropContext/Droppable/Draggable/DropResult/ResponderProvided) and React 19 support, so the only change is the import specifier (here and in preview-settings.tsx).
| "overrides": { | ||
| "dompurify": "^3.4.0", | ||
| "prismjs": "^1.30.0", | ||
| "baseline-browser-mapping": "2.10.33" | ||
| "baseline-browser-mapping": "2.10.33", | ||
| "memoize-one": "^6.0.0", | ||
| "@types/react": "^19.2.17", | ||
| "@types/react-dom": "^19.2.2" | ||
| }, | ||
| "resolutions": { | ||
| "baseline-browser-mapping": "2.10.33" | ||
| "baseline-browser-mapping": "2.10.33", | ||
| "memoize-one": "^6.0.0", | ||
| "@types/react": "^19.2.17", | ||
| "@types/react-dom": "^19.2.2" |
There was a problem hiding this comment.
not sure if we need either of the overrides/resolutions
There was a problem hiding this comment.
Good instinct — I tested it. Only memoize-one is actually needed: removing react-beautiful-dnd (which pinned memoize-one@^5) lets bun re-hoist the untyped memoize-one@4.1.0 from react-highlight-words, and src/config.ts imports it directly → TS7016 (no bundled types). Pinning memoize-one@^6 (typed; the version @hello-pangea/dnd and react-select already want) fixes it.
The @types/react / @types/react-dom overrides turned out not to be needed after the rebase — a single @types/react@19.2.17 now resolves on its own — so I dropped those. Kept memoize-one in both overrides and resolutions to match the existing baseline-browser-mapping pin (npm reads overrides, the yarn.lock side reads resolutions).
b45493e to
b920bdb
Compare
Upgrade Console's frontend from React 18.3.1 to React 19.2.x while keeping
Module Federation working with the React-18 Cloud UI host via
@module-federation/bridge-react.
- shared: {} so the bridge isolates Console's own React 19
- ./BridgeApp expose (createBridgeComponent from /v19); ./App becomes a
React-18-safe legacy shim so the current Cloud UI keeps working with no
host change
- react-beautiful-dnd -> @hello-pangea/dnd (react-redux 7's
unstable_batchedUpdates is removed in React 19)
- framer-motion@7 -> motion/react (React 19 compatible)
- build shims: react-onclickoutside (findDOMNode) and a react-router-dom
alias override to neutralize bridge-react's router hijack
- restore the global JSX namespace for @redpanda-data/ui's bundled
react-markdown@8
- React 19 type fixes via types-react-codemod (scoped JSX, useRef, RefObject)
- pin memoize-one to a typed version (removing react-beautiful-dnd re-hoisted
an untyped memoize-one@4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b920bdb to
125adf4
Compare
What
Upgrade Console's frontend from React 18.3.1 → React 19.2.x while keeping Module Federation working with the Cloud UI host, which stays on React 18.
Why the obvious upgrade breaks MF
rp_consolesharesreact/react-dom/@tanstack/*/react-hook-form/zodas singleton modules with Cloud UI. With a singleton, MF picks one React for the whole page — the React-18 host wins and feeds it to the remote. React-19-compiled remote code then runs against the React-18 runtime → "invalid hook call". You cannot run two React majors undersingleton: true.This is the same problem adp-ui solved in cloudv2#27303; admin-ui (cloudv2#27324) is the simpler standalone reference.
The fix:
@module-federation/bridge-react@2.5.1The bridge changes the host↔remote contract from "remote returns a React element rendered into the host tree" to "remote returns a
{ render, destroy }object that mounts into a host-supplied DOM node using its own React." That decouples the two React majors.module-federation.config.ts:shared: {}(nothing shared) so Console bundles its own React 19, react-query, router, etc../BridgeAppexpose (src/federation/console-federated-bridge.tsx) —createBridgeComponentfrom@module-federation/bridge-react/v19(the default entry calls the legacyrenderAPI and throws on React 19). Wraps the unchangedConsoleApp; sameConsoleAppPropscontract../Appbecomes a React-18-safe legacy shim (src/federation/console-legacy-app.tsx, ported from adp-ui). It returns a React-18 element shape whose ref mounts the real React 19 tree via Console's owncreateRoot— so the current Cloud UI host keeps renderingrp_console/Appwith no Cloud UI change. New hosts move to./BridgeApp.target: '18' → '19'.The non-obvious parts (Console-specific, not present in adp-ui)
adp-ui uses the vendored UI registry; Console uses the
@redpanda-data/ui@4.2.0package and a few legacy deps, which surfaced extra React-19 work:react-router-domalias override. Installing@module-federation/bridge-reactauto-activates a webpack plugin that aliasesreact-router-dom$to its own router shim. Because Console declares no directreact-router-dom, the plugin falls back to its v6 shim (only exportsBrowserRouter/RouterProvider) and breaks@redpanda-data/ui, which importsNavLink/Linkfrom the real react-router-dom@7. Console doesn't federate routing (it uses@tanstack/react-router), sorsbuild.config.tspointsreact-router-dom$back at the real package (the plugin spreads the user alias last, so it wins).react-beautiful-dnd→@hello-pangea/dnd. rbd pullsreact-redux@7, which callsReactDOM.unstable_batchedUpdates(removed in React 19) at module init → DnD would crash at runtime.@hello-pangea/dndis the maintained drop-in (identical API). 2 files.framer-motion@7→motion/react. framer-motion@7'smotion.*typings break under@types/react@19(props resolve tounknown). Console already shipsmotion@12, so the 7framer-motionimports move tomotion/react.react-onclickoutsidebuild shim. Statically importsfindDOMNode(removed in React 19) via@redpanda-data/ui → react-datepicker; breaks ESM linking. Console renders no datepicker → identity-HOC shim (mirrors admin-ui).JSXnamespace shim.@types/react@19removed the globalJSXnamespace.@redpanda-data/uibundlesreact-markdown@8, whosecomplex-types.tsships as source (soskipLibCheckcan't skip it) and useskeyof JSX.IntrinsicElements. A type-only ambient declaration restoresJSXas an alias ofReact.JSX.@types/react/memoize-oneviaoverrides/resolutions(the dnd swap had re-hoisted an untypedmemoize-one@4; a stray@types/react@18lingered under two@types/*deps).types-react-codemod(scoped JSX,useRef,RefObject).Verification (all Console
frontend/CI gates, run locally)bun install --frozen-lockfilebun run type:check(tsgo)bun run lint(ultracite)bun run test:unitbun run test:integrationact()warning incatalog-tree.test.tsxpre-exists onmaster)bun run build(rsbuild)./App+./BridgeApp,shared: []; no findDOMNode link errorbun run doctor(react-doctor)Cloud UI coordination (please read before merge)
rp_console/Appkeeps working unchanged via the legacy shim — the safety net, no hard cutover.rp_console/BridgeAppis the new primary path. A coordinated cloudv2 / Cloud UI PR switchesconsole-loaderto consume it viacreateRemoteAppComponent(mirrors the adp-loader change). (Companion PR follows.)rp_console/connect-tiles: the serverless onboarding wizard rendersConnectTilesdirectly in Cloud UI's React-18 tree and holds an imperativetriggerSubmit()ref, which the element-shape shim can't carry. It is gated behind the default-offenable-serverless-onboarding-wizardflag, so this draft is safe to review without it; the fix is tracked as a scoped follow-up in the companion PR.Known follow-ups
react-compiler-runtimeis now unused withtarget: '19'(React 19 shipsreact/compiler-runtime) — left in place to keep this diff focused.act()warning incatalog-tree.test.tsx(SQL studio) is unrelated to this change.🤖 Generated with Claude Code