From 893a780846ccb5d2ec87f83f2871c34874b6f94e Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 5 May 2026 09:47:40 -0700 Subject: [PATCH 1/2] feat(frontend): enable React StrictMode at root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the three React 18 root mounts (views/index, views/menu, embedded) in . Refs #39890. StrictMode is dev-only — production builds are unchanged. The RTL test wrapper does not enable StrictMode, so the existing test suite is unaffected. Any double-invocation/cleanup issues that surface during local dev work should be tracked as follow-up items. Co-Authored-By: Claude Sonnet 4.6 --- superset-frontend/src/embedded/index.tsx | 8 +++-- superset-frontend/src/views/index.tsx | 7 ++++- superset-frontend/src/views/menu.tsx | 39 +++++++++++++----------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx index a7437968678e..15b674cb0b8f 100644 --- a/superset-frontend/src/embedded/index.tsx +++ b/superset-frontend/src/embedded/index.tsx @@ -18,7 +18,7 @@ */ import 'src/public-path'; -import { lazy, Suspense } from 'react'; +import { lazy, StrictMode, Suspense } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import { Global } from '@emotion/react'; @@ -196,7 +196,11 @@ function start() { if (!root) { root = createRoot(appMountPoint); } - root.render(); + root.render( + + + , + ); }, err => { // something is most likely wrong with the guest token; reset the guard diff --git a/superset-frontend/src/views/index.tsx b/superset-frontend/src/views/index.tsx index e583f2bad863..456c1f09e924 100644 --- a/superset-frontend/src/views/index.tsx +++ b/superset-frontend/src/views/index.tsx @@ -18,6 +18,7 @@ */ import 'src/public-path'; +import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { logging } from '@apache-superset/core/utils'; import initPreamble from 'src/preamble'; @@ -31,7 +32,11 @@ if (appMountPoint) { await initPreamble(); } finally { const { default: App } = await import(/* webpackMode: "eager" */ './App'); - root.render(); + root.render( + + + , + ); } })().catch(err => { logging.error('Unhandled error during app initialization', err); diff --git a/superset-frontend/src/views/menu.tsx b/superset-frontend/src/views/menu.tsx index dda01a8e27ef..5ba511c70326 100644 --- a/superset-frontend/src/views/menu.tsx +++ b/superset-frontend/src/views/menu.tsx @@ -20,6 +20,7 @@ import 'src/public-path'; // Menu App. Used in views that do not already include the Menu component in the layout. // eg, backend rendered views +import { StrictMode } from 'react'; import { Provider } from 'react-redux'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; @@ -45,24 +46,26 @@ const emotionCache = createCache({ }); const app = ( - - - - - ) => - querystring.stringify(object, { encode: false }), - }} - > - - - - - - + + + + + + ) => + querystring.stringify(object, { encode: false }), + }} + > + + + + + + + ); const menuMountPoint = document.getElementById('app-menu'); From 04dcd888f87def0a18b3f135931f70560f21b8f0 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 5 May 2026 21:46:16 -0700 Subject: [PATCH 2/2] fix(embedded): move dataMask store.subscribe into an effect Per codeant review on #39893: subscribing to the Redux store from inside the render body of EmbededLazyDashboardPage registered a new listener on every render with no cleanup, leaking subscriptions and double-emitting observeDataMask events on each store update. StrictMode's dev-mode double-mount made the leak immediately visible. Move the subscription into a useEffect keyed on emitDataMasks, returning the Redux unsubscribe so React tears the listener down on unmount. Co-Authored-By: Claude Sonnet 4.6 --- superset-frontend/src/embedded/index.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/superset-frontend/src/embedded/index.tsx b/superset-frontend/src/embedded/index.tsx index 15b674cb0b8f..7b890272d802 100644 --- a/superset-frontend/src/embedded/index.tsx +++ b/superset-frontend/src/embedded/index.tsx @@ -18,7 +18,7 @@ */ import 'src/public-path'; -import { lazy, StrictMode, Suspense } from 'react'; +import { lazy, StrictMode, Suspense, useEffect } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import { Global } from '@emotion/react'; @@ -68,18 +68,19 @@ const LazyDashboardPage = lazy( const EmbededLazyDashboardPage = () => { const uiConfig = useUiConfig(); + const emitDataMasks = uiConfig?.emitDataMasks; - // Emit data mask changes to the parent window - if (uiConfig?.emitDataMasks) { + // Emit data mask changes to the parent window. Subscribing inside an effect + // (rather than during render) ensures the unsubscribe runs on unmount, + // including StrictMode's dev-mode double-mount cycle. + useEffect(() => { + if (!emitDataMasks) return undefined; log('setting up Switchboard event emitter'); let previousDataMask = store.getState().dataMask; - store.subscribe(() => { - const currentState = store.getState(); - const currentDataMask = currentState.dataMask; - - // Only emit if the dataMask has changed + return store.subscribe(() => { + const currentDataMask = store.getState().dataMask; if (previousDataMask !== currentDataMask) { Switchboard.emit('observeDataMask', { ...currentDataMask, @@ -88,7 +89,7 @@ const EmbededLazyDashboardPage = () => { previousDataMask = currentDataMask; } }); - } + }, [emitDataMasks]); return ; };