Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Code
04dcd888f8 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 <noreply@anthropic.com>
2026-05-05 21:46:16 -07:00
Claude Code
893a780846 feat(frontend): enable React StrictMode at root
Wrap the three React 18 root mounts (views/index, views/menu, embedded)
in <StrictMode>. 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 <noreply@anthropic.com>
2026-05-05 09:47:40 -07:00
3 changed files with 42 additions and 29 deletions

View File

@@ -18,7 +18,7 @@
*/
import 'src/public-path';
import { lazy, 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 <LazyDashboardPage idOrSlug={bootstrapData.embedded!.dashboard_id} />;
};
@@ -196,7 +197,11 @@ function start() {
if (!root) {
root = createRoot(appMountPoint);
}
root.render(<EmbeddedApp />);
root.render(
<StrictMode>
<EmbeddedApp />
</StrictMode>,
);
},
err => {
// something is most likely wrong with the guest token; reset the guard

View File

@@ -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(<App />);
root.render(
<StrictMode>
<App />
</StrictMode>,
);
}
})().catch(err => {
logging.error('Unhandled error during app initialization', err);

View File

@@ -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 = (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Provider store={store}>
<BrowserRouter>
<QueryParamProvider
adapter={ReactRouter5Adapter}
options={{
searchStringToObject: querystring.parse,
objectToSearchString: (object: Record<string, any>) =>
querystring.stringify(object, { encode: false }),
}}
>
<Menu data={menu} />
</QueryParamProvider>
</BrowserRouter>
</Provider>
</ThemeProvider>
</CacheProvider>
<StrictMode>
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Provider store={store}>
<BrowserRouter>
<QueryParamProvider
adapter={ReactRouter5Adapter}
options={{
searchStringToObject: querystring.parse,
objectToSearchString: (object: Record<string, any>) =>
querystring.stringify(object, { encode: false }),
}}
>
<Menu data={menu} />
</QueryParamProvider>
</BrowserRouter>
</Provider>
</ThemeProvider>
</CacheProvider>
</StrictMode>
);
const menuMountPoint = document.getElementById('app-menu');