Skip to content

Commit 81b6c53

Browse files
feat(frontend): migrate to React 19 via Module Federation bridge (#2525)
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>
1 parent 6b35cc5 commit 81b6c53

52 files changed

Lines changed: 733 additions & 515 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/bun.lock

Lines changed: 52 additions & 105 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/module-federation.config.ts

Lines changed: 19 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
import type { ModuleFederationPluginOptions } from '@module-federation/rsbuild-plugin';
1313

14-
const deps: Record<string, string> = (await import('./package.json', { with: { type: 'json' } })).default.dependencies;
15-
1614
export const moduleFederationConfig: ModuleFederationPluginOptions = {
1715
name: 'rp_console',
1816
// IMPORTANT: Keep as 'embedded.js' for backward compatibility with existing Cloud UI
@@ -22,8 +20,17 @@ export const moduleFederationConfig: ModuleFederationPluginOptions = {
2220
},
2321

2422
exposes: {
25-
// V2: New component pattern for Cloud UI MF v2.0 integration
26-
'./App': './src/federation/console-app.tsx',
23+
// './App' is a React 18-safe compatibility shim. Console runs React 19, but
24+
// the current Cloud UI host (React 18) still loads 'rp_console/App' and
25+
// renders it as a plain element. The shim returns a React 18 element shape
26+
// whose ref mounts the real React 19 app via Console's own createRoot, so
27+
// the existing host keeps working with no Cloud UI change. New Cloud UI
28+
// hosts consume './BridgeApp' instead.
29+
'./App': './src/federation/console-legacy-app.tsx',
30+
// React bridge entry consumed by Cloud UI. Mounts the app with Console's own
31+
// React 19 createRoot into a host-provided DOM node, decoupling it from the
32+
// host's React 18. See src/federation/console-federated-bridge.tsx.
33+
'./BridgeApp': './src/federation/console-federated-bridge.tsx',
2734
'./types': './src/federation/types.ts',
2835

2936
// Legacy: Keep for backward compat with old Cloud UI
@@ -33,63 +40,12 @@ export const moduleFederationConfig: ModuleFederationPluginOptions = {
3340
'./config': './src/config.ts',
3441
},
3542

36-
shared: {
37-
react: {
38-
singleton: true,
39-
requiredVersion: deps.react,
40-
eager: false,
41-
},
42-
'react-dom': {
43-
singleton: true,
44-
requiredVersion: deps['react-dom'],
45-
eager: false,
46-
},
47-
'@redpanda-data/ui': {
48-
import: '@redpanda-data/ui',
49-
},
50-
'@tanstack/react-query': {
51-
singleton: true,
52-
requiredVersion: deps['@tanstack/react-query'],
53-
eager: false,
54-
},
55-
'@tanstack/react-router': {
56-
singleton: true,
57-
requiredVersion: deps['@tanstack/react-router'],
58-
eager: false,
59-
},
60-
'react-hook-form': {
61-
singleton: true,
62-
requiredVersion: deps['react-hook-form'],
63-
},
64-
'@hookform/resolvers': {
65-
singleton: true,
66-
requiredVersion: deps['@hookform/resolvers'],
67-
},
68-
zod: {
69-
singleton: true,
70-
requiredVersion: deps.zod,
71-
},
72-
// lucide-react is intentionally NOT shared. It is stateless SVG components
73-
// (no context/hooks), so it has no singleton requirement. Sharing it as a
74-
// singleton forced the whole package (~1.5 MiB / 1700+ icons) into a shared
75-
// chunk regardless of which icons are used; unshared, this remote tree-shakes
76-
// to only the icons it imports and can upgrade lucide independently of the
77-
// cloud-ui host. The host and the adp-ui remote unshare it too.
78-
'class-variance-authority': {
79-
singleton: false,
80-
requiredVersion: deps['class-variance-authority'],
81-
},
82-
'tailwind-merge': {
83-
singleton: false,
84-
requiredVersion: deps['tailwind-merge'],
85-
},
86-
motion: {
87-
singleton: false,
88-
requiredVersion: deps.motion,
89-
},
90-
clsx: {
91-
singleton: false,
92-
requiredVersion: deps.clsx,
93-
},
94-
},
43+
// Nothing is shared. Console runs React 19 while Cloud UI stays on React 18; a
44+
// shared React singleton cannot span both majors. The React bridge
45+
// ('./BridgeApp') isolates Console's React 19 along with its own
46+
// @tanstack/react-query, @tanstack/react-router, react-hook-form and the rest,
47+
// so each app bundles its own copies and no cross-major React conflict can
48+
// occur in the embedded scene. (lucide-react was never shared either —
49+
// stateless SVGs with no singleton requirement.)
50+
shared: {},
9551
};

frontend/package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@
7070
"@connectrpc/connect-query": "^2.2.0",
7171
"@connectrpc/connect-web": "^2.1.0",
7272
"@emotion/css": "^11.13.5",
73+
"@hello-pangea/dnd": "^18.0.1",
7374
"@hookform/resolvers": "^5.2.2",
7475
"@icons-pack/react-simple-icons": "^13.8.0",
7576
"@lezer/highlight": "^1.2.3",
7677
"@milkdown/kit": "^7.18.0",
7778
"@milkdown/react": "^7.18.0",
7879
"@modelcontextprotocol/sdk": "^1.29.0",
80+
"@module-federation/bridge-react": "2.5.1",
7981
"@module-federation/runtime": "^2.5.1",
8082
"@monaco-editor/react": "^4.7.0",
8183
"@redpanda-data/ui": "^4.2.0",
@@ -101,7 +103,6 @@
101103
"dexie": "^4.2.1",
102104
"dotenv": "^17.2.3",
103105
"es-cookie": "^1.5.0",
104-
"framer-motion": "^7.10.3",
105106
"hast": "^1.0.0",
106107
"hast-util-to-jsx-runtime": "^2.3.6",
107108
"js-base64": "^3.7.8",
@@ -117,12 +118,11 @@
117118
"pretty-bytes": "^5.6.0",
118119
"pretty-ms": "^7.0.1",
119120
"prismjs": "^1.30.0",
120-
"react": "^18.3.1",
121-
"react-beautiful-dnd": "^13.1.1",
121+
"react": "^19.2.0",
122122
"react-compiler-runtime": "^1.0.0",
123123
"react-data-grid": "7.0.0-beta.47",
124124
"react-day-picker": "^9.14.0",
125-
"react-dom": "^18.3.1",
125+
"react-dom": "^19.2.0",
126126
"react-dropzone": "^15.0.0",
127127
"react-highlight-words": "^0.21.0",
128128
"react-hook-form": "^7.76.1",
@@ -173,9 +173,8 @@
173173
"@testing-library/user-event": "^14.6.1",
174174
"@types/json-bigint": "^1.0.4",
175175
"@types/node": "^22.19.1",
176-
"@types/react": "^18.3.26",
177-
"@types/react-beautiful-dnd": "^13.1.8",
178-
"@types/react-dom": "^18.3.7",
176+
"@types/react": "^19.2.17",
177+
"@types/react-dom": "^19.2.2",
179178
"@types/react-highlight-words": "^0.20.0",
180179
"@types/react-syntax-highlighter": "^15.5.13",
181180
"@typescript/native-preview": "^7.0.0-dev.20260108.1",
@@ -204,10 +203,12 @@
204203
"overrides": {
205204
"dompurify": "^3.4.0",
206205
"prismjs": "^1.30.0",
207-
"baseline-browser-mapping": "2.10.33"
206+
"baseline-browser-mapping": "2.10.33",
207+
"memoize-one": "^6.0.0"
208208
},
209209
"resolutions": {
210-
"baseline-browser-mapping": "2.10.33"
210+
"baseline-browser-mapping": "2.10.33",
211+
"memoize-one": "^6.0.0"
211212
},
212213
"patchedDependencies": {
213214
"@tanstack/react-router@1.170.15": "patches/@tanstack%2Freact-router@1.170.15.patch"

frontend/rsbuild.config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default defineConfig({
3535
opts.plugins.unshift([
3636
'babel-plugin-react-compiler',
3737
{
38-
target: '18',
38+
target: '19',
3939
compilationMode: 'annotation',
4040
panicThreshold: 'critical_errors',
4141
// In annotation mode, this still gates which files CAN be opted in.
@@ -164,9 +164,24 @@ export default defineConfig({
164164

165165
// Stub `date-fns-tz` v2 imports from `@redpanda-data/ui` — see
166166
// `src/utils/vendor/date-fns-tz-shim.ts` for context.
167+
//
168+
// react-onclickoutside (transitive via `@redpanda-data/ui`'s react-datepicker)
169+
// statically imports `findDOMNode`, which React 19 removed — this breaks the
170+
// bundle's ESM linking. Console renders no datepicker, so redirect it to an
171+
// identity-HOC shim. See `src/shims/react-onclickoutside-shim.ts`.
172+
//
173+
// `@module-federation/bridge-react` auto-installs a webpack-plugin that aliases
174+
// `react-router-dom$` to its own router shim. Because Console declares no direct
175+
// react-router-dom dependency, that plugin falls back to its v6 shim (only
176+
// exports BrowserRouter/RouterProvider) and breaks `@redpanda-data/ui`, which
177+
// imports NavLink/Link from the real react-router-dom@7. Console does not federate
178+
// routing (it uses @tanstack/react-router), so point react-router-dom back at the
179+
// real package — the plugin spreads the user alias last, so this override wins.
167180
Object.assign(config.resolve.alias as Record<string, string>, {
168181
'date-fns-tz$': path.resolve(__dirname, 'src/utils/vendor/date-fns-tz-shim.ts'),
169182
'date-fns-tz/zonedTimeToUtc$': path.resolve(__dirname, 'src/utils/vendor/zonedTimeToUtc.ts'),
183+
'react-onclickoutside': path.resolve(__dirname, 'src/shims/react-onclickoutside-shim.ts'),
184+
'react-router-dom$': path.resolve(__dirname, 'node_modules/react-router-dom'),
170185
});
171186

172187
config.output.publicPath = 'auto';

frontend/src/components/misc/broker-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import { Tooltip } from '@redpanda-data/ui';
1313
import { ChevronRightIcon } from 'components/icons';
14-
import React, { Component } from 'react';
14+
import React, { Component, type JSX } from 'react';
1515

1616
import { api, brokerMap } from '../../state/backend-api';
1717
import type { Broker, Partition } from '../../state/rest-interfaces';

frontend/src/components/misc/common.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@redpanda-data/ui';
1313
import { AlertIcon } from 'components/icons';
14-
import React, { type PropsWithChildren, useState } from 'react';
14+
import React, { type JSX, type PropsWithChildren, useState } from 'react';
1515

1616
import type { TopicLogDirSummary } from '../../state/rest-interfaces';
1717
import { uiState } from '../../state/ui-state';

frontend/src/components/misc/config-list.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { Box, DataTable, Flex, Text, Tooltip } from '@redpanda-data/ui';
1313
import type { ColumnDef } from '@tanstack/react-table';
1414
import { EyeOffIcon, InfoIcon } from 'components/icons';
15+
import type { JSX } from 'react';
1516

1617
import styles from './ConfigList.module.scss';
1718
import colors from '../../colors';

frontend/src/components/misc/hidden-radio-list.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

12+
import type { JSX } from 'react';
13+
1214
import styles from './HiddenRadioList.module.scss';
1315

1416
export type HiddenRadioOption<ValueType> = {

frontend/src/components/misc/page-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Stack } from '@redpanda-data/ui';
2-
import { motion } from 'framer-motion';
2+
import { motion } from 'motion/react';
33
import type { ReactNode } from 'react';
44

55
import { animProps } from '../../utils/animation-props';

frontend/src/components/misc/small-stat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Flex, Text } from '@redpanda-data/ui';
2+
import type { JSX } from 'react';
23

34
export function SmallStat(p: { title: JSX.Element | string; children: JSX.Element | number | string }) {
45
return (

0 commit comments

Comments
 (0)