feat(frontend): enable React StrictMode at root#39893
feat(frontend): enable React StrictMode at root#39893
Conversation
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>
Code Review Agent Run #541f49Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
| root.render( | ||
| <StrictMode> | ||
| <EmbeddedApp /> | ||
| </StrictMode>, | ||
| ); |
There was a problem hiding this comment.
Suggestion: Wrapping the embedded tree in StrictMode will trigger an extra mount/unmount cycle in development, and this code path includes a Redux store.subscribe side effect inside render (EmbededLazyDashboardPage) without cleanup. That causes duplicate active subscriptions and repeated observeDataMask emissions after startup. Move the subscription logic into an effect with cleanup (or gate StrictMode here until that cleanup is implemented) so StrictMode does not create leaked duplicate listeners. [memory leak]
Severity Level: Major ⚠️
- ❌ Embedded dev dashboards emit duplicate observeDataMask events per change.
- ⚠️ Host apps receive duplicated observeDataMask callbacks in development.
- ⚠️ Redux store subscriptions accumulate without cleanup in embedded iframe.Steps of Reproduction ✅
1. In a host app, call `embedDashboard` from `superset-embedded-sdk/src/index.ts` with
`dashboardUiConfig.emitDataMasks: true` (see `UiConfigType.emitDataMasks` at lines 36-40
and `calculateConfig()` adding `+16` for `emitDataMasks` at lines 143-156, then
`dashboardConfigUrlParams` including `uiConfig` at lines 168-170). This generates an
iframe src like `/embedded/<id>?uiConfig=<bitmask>`.
2. The iframe loads the embedded frontend route defined in
`superset-frontend/src/embedded/index.tsx` where `EmbeddedApp` (lines 117-123) renders
`<Route path="/embedded/:uuid/" component={EmbeddedRoute} />`. `EmbeddedRoute` (lines
96-115) wraps children in `EmbeddedContextProviders`, which in turn wraps them in
`EmbeddedUiConfigProvider` (`superset-frontend/src/components/UiConfigContext/index.tsx`
lines 48-66).
3. Inside `EmbeddedUiConfigProvider`, `getUrlParam(URL_PARAMS.uiConfig)` (lines 51-59 of
`UiConfigContext/index.tsx`) decodes the `uiConfig` bitmask and sets `emitDataMasks:
(config & 16) !== 0`. `EmbededLazyDashboardPage` in
`superset-frontend/src/embedded/index.tsx` (lines 69-94) calls `useUiConfig()`, and when
`uiConfig.emitDataMasks` is true, it executes the side-effect block at lines 73-91 that
declares `let previousDataMask = store.getState().dataMask;` and calls `store.subscribe(()
=> { ... Switchboard.emit('observeDataMask', ...) })` at lines 76-88, with no captured
unsubscribe function or cleanup.
4. The embedded app is started when the iframe's `MessageChannel` handshake triggers
`Switchboard.defineMethod('guestToken', ...)` in
`superset-frontend/src/embedded/index.tsx` (lines 259-264), which calls
`setupGuestClient(guestToken)` and then `start()`. In `start()` (same file, lines
179-217), after fetching `/api/v1/me/roles/`, the code creates the React root if needed
and then calls `root.render(<StrictMode><EmbeddedApp /></StrictMode>);` at lines 197-203
(the PR's new code).
5. In development builds, React 18 `StrictMode` intentionally double-invokes function
components on initial mount. As a result, `EmbededLazyDashboardPage`'s function body
(including the `store.subscribe` call at line 78) runs twice, registering two Redux
subscriptions without any corresponding unsubscribe on unmount. On each subsequent
data-mask-related store update, both subscribers execute, each calling
`Switchboard.emit('observeDataMask', ...)` (lines 84-87), so the host's `observeDataMask`
callback registered via `ourPort.defineMethod('observeDataMask', callbackFn)` in
`superset-embedded-sdk/src/index.ts` (lines 34-38 of the second chunk) is invoked multiple
times per state change. Because no cleanup exists, these subscriptions persist for the
lifetime of the page, creating leaked duplicate listeners specifically exposed by the new
StrictMode wrapper.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset-frontend/src/embedded/index.tsx
**Line:** 199:203
**Comment:**
*Memory Leak: Wrapping the embedded tree in `StrictMode` will trigger an extra mount/unmount cycle in development, and this code path includes a Redux `store.subscribe` side effect inside render (`EmbededLazyDashboardPage`) without cleanup. That causes duplicate active subscriptions and repeated `observeDataMask` emissions after startup. Move the subscription logic into an effect with cleanup (or gate StrictMode here until that cleanup is implemented) so StrictMode does not create leaked duplicate listeners.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThere was a problem hiding this comment.
Good catch — fix pushed in 04dcd88. The store.subscribe was inside the render body without cleanup, which (independently of StrictMode) leaked a new subscription on every re-render. Moved it into a useEffect keyed on emitDataMasks, returning the Redux unsubscribe so it tears down on unmount and on the StrictMode dev-mode double-mount. The pre-existing latent leak is now fixed too.
|
Yes, the suggestion is valid — StrictMode's double-invocation in development causes the store.subscribe in EmbededLazyDashboardPage's render to create duplicate subscriptions without cleanup, leading to memory leaks and repeated observeDataMask emissions. To resolve, move the subscription logic into useEffect with cleanup: import useEffect, then wrap the subscribe call in useEffect(() => { const unsubscribe = store.subscribe(...); return unsubscribe; }, [uiConfig.emitDataMasks]). There are no other comments on this PR. superset-frontend/src/embedded/index.tsx |
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #39893 +/- ##
==========================================
- Coverage 64.38% 64.38% -0.01%
==========================================
Files 2569 2569
Lines 134785 134786 +1
Branches 31295 31295
==========================================
Hits 86777 86777
- Misses 46510 46511 +1
Partials 1498 1498
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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>
Code Review Agent Run #4a46a4Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
SUMMARY
First foundational step for #39890 (React 18 concurrent-feature adoption).
Wraps the three React 18 root mounts in
<StrictMode>:superset-frontend/src/views/index.tsx(main app)superset-frontend/src/views/menu.tsx(backend-rendered views' menu)superset-frontend/src/embedded/index.tsx(embedded SDK)Why
Per the React 18 docs, StrictMode's dev-mode double-invocation surfaces effect-cleanup and non-idempotent-render bugs that concurrent rendering will eventually expose at runtime. Turning it on now lets us catch those deliberately during local dev rather than have them appear later as "weird remount" or stale-state regressions.
Scope notes
spec/helpers/testing-library.tsxdoes not enable StrictMode, so existing tests behave identically.BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
N/A — no UI change. Dev-mode console may now show second-invocation warnings for components with cleanup bugs; those are pre-existing latent issues to be fixed in follow-ups.
TESTING INSTRUCTIONS
npm run devfromsuperset-frontend/.npm run buildto confirm production builds still succeed (StrictMode is stripped in prod).ADDITIONAL INFORMATION