You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/05 17:58:51 UTC

(superset) branch master updated: chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 550c80f640a chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
550c80f640a is described below

commit 550c80f640a7fad2fcbdc62cac65085cb24063ff
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Fri Jun 5 10:58:44 2026 -0700

    chore(lint): convert ChartRenderer, Chart, DrillByChart to function components (#39459)
    
    Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
    Co-authored-by: Claude <cl...@anthropic.com>
---
 superset-frontend/src/components/Chart/Chart.tsx   | 506 +++++++-------
 .../src/components/Chart/ChartRenderer.test.tsx    |  83 +--
 .../src/components/Chart/ChartRenderer.tsx         | 732 ++++++++++-----------
 .../src/components/Chart/DrillBy/DrillByChart.tsx  |   4 +-
 4 files changed, 665 insertions(+), 660 deletions(-)

diff --git a/superset-frontend/src/components/Chart/Chart.tsx b/superset-frontend/src/components/Chart/Chart.tsx
index 5f058aa6024..1519837c747 100644
--- a/superset-frontend/src/components/Chart/Chart.tsx
+++ b/superset-frontend/src/components/Chart/Chart.tsx
@@ -16,9 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ErrorInfo, PureComponent } from 'react';
-import { logging } from '@apache-superset/core/utils';
+import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
 import { t } from '@apache-superset/core/translation';
+import { logging } from '@apache-superset/core/utils';
 import {
   ensureIsArray,
   FeatureFlag,
@@ -60,7 +60,7 @@ export interface ChartProps {
   sharedLabelColors?: string;
   width: number;
   height: number;
-  setControlValue: (name: string, value: unknown) => void;
+  setControlValue?: (name: string, value: unknown) => void;
   timeout?: number;
   vizType: string;
   triggerRender?: boolean;
@@ -69,7 +69,7 @@ export interface ChartProps {
   chartAlert?: string;
   chartStatus?: ChartStatus;
   chartStackTrace?: string;
-  queriesResponse: ChartState['queriesResponse'];
+  queriesResponse?: ChartState['queriesResponse'];
   latestQueryFormData?: ChartState['latestQueryFormData'];
   triggerQuery?: boolean;
   chartIsStale?: boolean;
@@ -126,19 +126,6 @@ const NONEXISTENT_DATASET = t(
   'The dataset associated with this chart no longer exists',
 );
 
-const defaultProps: Partial<ChartProps> = {
-  addFilter: () => BLANK,
-  onFilterMenuOpen: () => BLANK,
-  onFilterMenuClose: () => BLANK,
-  initialValues: BLANK,
-  setControlValue: () => BLANK,
-  triggerRender: false,
-  dashboardId: undefined,
-  chartStackTrace: undefined,
-  force: false,
-  isInView: true,
-};
-
 const Styles = styled.div<{ height: number; width?: number }>`
   min-height: ${p => p.height}px;
   position: relative;
@@ -186,252 +173,321 @@ const MessageSpan = styled.span`
   color: ${({ theme }) => theme.colorText};
 `;
 
-class Chart extends PureComponent<ChartProps, {}> {
-  static defaultProps = defaultProps;
-
-  renderStartTime: number;
-
-  constructor(props: ChartProps) {
-    super(props);
-    this.renderStartTime = Logger.getTimestamp();
-    this.handleRenderContainerFailure =
-      this.handleRenderContainerFailure.bind(this);
-  }
-
-  componentDidMount() {
-    if (this.props.triggerQuery) {
-      this.runQuery();
-    }
-  }
-
-  componentDidUpdate() {
-    if (this.props.triggerQuery) {
-      this.runQuery();
-    }
-  }
-
-  shouldRenderChart() {
-    return (
-      this.props.isInView ||
+function Chart({
+  addFilter = () => BLANK,
+  onFilterMenuOpen = () => BLANK,
+  onFilterMenuClose = () => BLANK,
+  initialValues = BLANK,
+  setControlValue = () => BLANK,
+  triggerRender = false,
+  dashboardId,
+  chartStackTrace,
+  force = false,
+  isInView = true,
+  ...restProps
+}: ChartProps): JSX.Element {
+  const {
+    actions,
+    chartId,
+    datasource,
+    formData,
+    timeout,
+    ownState,
+    chartAlert,
+    chartStatus,
+    queriesResponse = [],
+    errorMessage,
+    chartIsStale,
+    width,
+    height,
+    datasetsStatus,
+    onQuery,
+    annotationData,
+    vizType,
+    latestQueryFormData,
+    triggerQuery,
+    postTransformProps,
+    emitCrossFilters,
+    onChartStateChange,
+    suppressLoadingSpinner,
+    filterState,
+  } = restProps;
+
+  const renderStartTimeRef = useRef<number>(Logger.getTimestamp());
+  // Update on each render to accurately track render duration
+  renderStartTimeRef.current = Logger.getTimestamp();
+
+  const shouldRenderChart = useCallback(
+    () =>
+      isInView ||
       !isFeatureEnabled(FeatureFlag.DashboardVirtualization) ||
-      isCurrentUserBot()
-    );
-  }
+      isCurrentUserBot(),
+    [isInView],
+  );
 
-  runQuery() {
+  const runQuery = useCallback(() => {
     if (
       isFeatureEnabled(FeatureFlag.DashboardVirtualizationDeferData) &&
-      !this.shouldRenderChart()
+      !shouldRenderChart()
     ) {
       return;
     }
     // Create chart with POST request
-    this.props.actions.postChartFormData(
-      this.props.formData,
-      Boolean(this.props.force || getUrlParam(URL_PARAMS.force)), // allow override via url params force=true
-      this.props.timeout,
-      this.props.chartId,
-      this.props.dashboardId,
-      this.props.ownState,
-    );
-  }
-
-  handleRenderContainerFailure(error: Error, info: ErrorInfo) {
-    const { actions, chartId } = this.props;
-    logging.warn(error);
-    actions.chartRenderingFailed(
-      error.toString(),
+    actions.postChartFormData(
+      formData,
+      Boolean(force || getUrlParam(URL_PARAMS.force)), // allow override via url params force=true
+      timeout,
       chartId,
-      info?.componentStack ?? null,
+      dashboardId,
+      ownState,
     );
+  }, [
+    actions,
+    chartId,
+    dashboardId,
+    formData,
+    force,
+    ownState,
+    shouldRenderChart,
+    timeout,
+  ]);
+
+  const handleRenderContainerFailure = useCallback(
+    (error: Error, info: ErrorInfo) => {
+      logging.warn(error);
+      actions.chartRenderingFailed(
+        error.toString(),
+        chartId,
+        info?.componentStack ?? null,
+      );
 
-    actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
-      slice_id: chartId,
-      has_err: true,
-      error_details: error.toString(),
-      start_offset: this.renderStartTime,
-      ts: new Date().getTime(),
-      duration: Logger.getTimestamp() - this.renderStartTime,
-    });
-  }
+      actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
+        slice_id: chartId,
+        has_err: true,
+        error_details: error.toString(),
+        start_offset: renderStartTimeRef.current,
+        ts: new Date().getTime(),
+        duration: Logger.getTimestamp() - renderStartTimeRef.current,
+      });
+    },
+    [actions, chartId],
+  );
 
-  renderErrorMessage(queryResponse: ChartErrorType) {
-    const {
-      chartId,
+  // componentDidMount and componentDidUpdate combined
+  useEffect(() => {
+    if (triggerQuery) {
+      runQuery();
+    }
+  }, [triggerQuery, runQuery]);
+
+  const renderErrorMessage = useCallback(
+    (queryResponse: ChartErrorType) => {
+      const error = queryResponse?.errors?.[0];
+      const message = chartAlert || queryResponse?.message;
+
+      // if datasource is still loading, don't render JS errors
+      // but always show backend API errors (which have an errors array)
+      // so users can see real issues like auth failures
+      if (
+        !error &&
+        chartAlert !== undefined &&
+        chartAlert !== NONEXISTENT_DATASET &&
+        datasource === PLACEHOLDER_DATASOURCE &&
+        datasetsStatus !== ResourceStatus.Error
+      ) {
+        return (
+          <Styles
+            key={chartId}
+            data-ui-anchor="chart"
+            className="chart-container"
+            data-test="chart-container"
+            height={height}
+          >
+            <Loading size={dashboardId ? 's' : 'm'} muted={!!dashboardId} />
+          </Styles>
+        );
+      }
+
+      return (
+        <ChartErrorMessage
+          key={chartId}
+          chartId={chartId}
+          error={error}
+          subtitle={message}
+          link={queryResponse ? queryResponse.link : undefined}
+          source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
+          stackTrace={chartStackTrace}
+        />
+      );
+    },
+    [
       chartAlert,
+      chartId,
       chartStackTrace,
-      datasource,
       dashboardId,
-      height,
       datasetsStatus,
-    } = this.props;
-    const error = queryResponse?.errors?.[0];
-    const message = chartAlert || queryResponse?.message;
+      datasource,
+      height,
+    ],
+  );
+
+  const renderSpinner = useCallback(
+    (databaseName: string | undefined) => {
+      const message = databaseName
+        ? t('Waiting on %s', databaseName)
+        : t('Waiting on database...');
 
-    // if datasource is still loading, don't render JS errors
-    // but always show backend API errors (which have an errors array)
-    // so users can see real issues like auth failures
-    if (
-      !error &&
-      chartAlert !== undefined &&
-      chartAlert !== NONEXISTENT_DATASET &&
-      datasource === PLACEHOLDER_DATASOURCE &&
-      datasetsStatus !== ResourceStatus.Error
-    ) {
       return (
-        <Styles
-          key={chartId}
-          data-ui-anchor="chart"
-          className="chart-container"
-          data-test="chart-container"
-          height={height}
-        >
+        <LoadingDiv>
           <Loading
-            size={this.props.dashboardId ? 's' : 'm'}
-            muted={!!this.props.dashboardId}
+            position="inline-centered"
+            size={dashboardId ? 's' : 'm'}
+            muted={!!dashboardId}
           />
-        </Styles>
+          <MessageSpan>{message}</MessageSpan>
+        </LoadingDiv>
       );
-    }
-
-    return (
-      <ChartErrorMessage
-        key={chartId}
-        chartId={chartId}
-        error={error}
-        subtitle={message}
-        link={queryResponse ? queryResponse.link : undefined}
-        source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
-        stackTrace={chartStackTrace}
-      />
-    );
-  }
-
-  renderSpinner(databaseName: string | undefined) {
-    const message = databaseName
-      ? t('Waiting on %s', databaseName)
-      : t('Waiting on database...');
-
-    return (
-      <LoadingDiv>
-        <Loading
-          position="inline-centered"
-          size={this.props.dashboardId ? 's' : 'm'}
-          muted={!!this.props.dashboardId}
-        />
-        <MessageSpan>{message}</MessageSpan>
-      </LoadingDiv>
-    );
-  }
+    },
+    [dashboardId],
+  );
 
-  renderChartContainer() {
-    return (
+  const renderChartContainer = useCallback(
+    () => (
       <div className="slice_container" data-test="slice-container">
-        {this.shouldRenderChart() ? (
+        {shouldRenderChart() ? (
           <ChartRenderer
-            {...this.props}
-            source={
-              this.props.dashboardId
-                ? ChartSource.Dashboard
-                : ChartSource.Explore
-            }
-            data-test={this.props.vizType}
+            annotationData={annotationData}
+            actions={actions}
+            chartId={chartId}
+            datasource={datasource}
+            initialValues={initialValues}
+            formData={formData}
+            height={height}
+            width={width}
+            setControlValue={setControlValue}
+            vizType={vizType}
+            triggerRender={triggerRender}
+            chartAlert={chartAlert}
+            chartStatus={chartStatus}
+            queriesResponse={queriesResponse}
+            triggerQuery={triggerQuery}
+            chartIsStale={chartIsStale}
+            addFilter={addFilter}
+            onFilterMenuOpen={onFilterMenuOpen}
+            onFilterMenuClose={onFilterMenuClose}
+            ownState={ownState}
+            postTransformProps={postTransformProps}
+            emitCrossFilters={emitCrossFilters}
+            onChartStateChange={onChartStateChange}
+            latestQueryFormData={latestQueryFormData}
+            filterState={filterState}
+            suppressLoadingSpinner={suppressLoadingSpinner}
+            source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
           />
         ) : (
-          <Loading
-            size={this.props.dashboardId ? 's' : 'm'}
-            muted={!!this.props.dashboardId}
-          />
+          <Loading size={dashboardId ? 's' : 'm'} muted={!!dashboardId} />
         )}
       </div>
-    );
-  }
-
-  render() {
-    const {
-      height,
+    ),
+    [
+      actions,
+      addFilter,
+      annotationData,
       chartAlert,
+      chartId,
+      chartIsStale,
       chartStatus,
+      dashboardId,
       datasource,
-      errorMessage,
-      chartIsStale,
-      queriesResponse = [],
+      emitCrossFilters,
+      filterState,
+      formData,
+      height,
+      initialValues,
+      latestQueryFormData,
+      onChartStateChange,
+      onFilterMenuClose,
+      onFilterMenuOpen,
+      ownState,
+      postTransformProps,
+      queriesResponse,
+      setControlValue,
+      shouldRenderChart,
+      suppressLoadingSpinner,
+      triggerQuery,
+      triggerRender,
+      vizType,
       width,
-    } = this.props;
-
-    const databaseName =
-      datasource?.parent?.name ??
-      (datasource?.database?.name as string | undefined);
+    ],
+  );
 
-    const isLoading = chartStatus === 'loading';
-    // Suppress spinner during auto-refresh to avoid visual flicker
-    const showSpinner = isLoading && !this.props.suppressLoadingSpinner;
+  const databaseName =
+    datasource?.parent?.name ??
+    (datasource?.database?.name as string | undefined);
 
-    if (chartStatus === 'failed') {
-      return (
-        <ErrorContainer height={height}>
-          {queriesResponse?.map(item =>
-            this.renderErrorMessage(item as ChartErrorType),
-          )}
-        </ErrorContainer>
-      );
-    }
+  const isLoading = chartStatus === 'loading';
+  // Suppress spinner during auto-refresh to avoid visual flicker
+  const showSpinner = isLoading && !suppressLoadingSpinner;
 
-    if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
-      return (
-        <EmptyState
-          size="large"
-          title={t('Add required control values to preview chart')}
-          description={getChartRequiredFieldsMissingMessage(true)}
-          image="chart.svg"
-        />
-      );
-    }
-    if (
-      !isLoading &&
-      !chartAlert &&
-      !errorMessage &&
-      chartIsStale &&
-      ensureIsArray(queriesResponse).length === 0
-    ) {
-      return (
-        <EmptyState
-          size="large"
-          title={t('Your chart is ready to go!')}
-          description={
-            <span>
-              {t(
-                'Click on "Create chart" button in the control panel on the left to preview a visualization or',
-              )}{' '}
-              <span role="button" tabIndex={0} onClick={this.props.onQuery}>
-                {t('click here')}
-              </span>
-              .
-            </span>
-          }
-          image="chart.svg"
-        />
-      );
-    }
+  if (chartStatus === 'failed') {
+    return (
+      <ErrorContainer height={height}>
+        {queriesResponse?.map(item =>
+          renderErrorMessage(item as ChartErrorType),
+        )}
+      </ErrorContainer>
+    );
+  }
 
+  if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
     return (
-      <ErrorBoundary
-        onError={this.handleRenderContainerFailure}
-        showMessage={false}
-      >
-        <Styles
-          data-ui-anchor="chart"
-          className="chart-container"
-          data-test="chart-container"
-          height={height}
-          width={width}
-        >
-          {showSpinner
-            ? this.renderSpinner(databaseName)
-            : this.renderChartContainer()}
-        </Styles>
-      </ErrorBoundary>
+      <EmptyState
+        size="large"
+        title={t('Add required control values to preview chart')}
+        description={getChartRequiredFieldsMissingMessage(true)}
+        image="chart.svg"
+      />
     );
   }
+  if (
+    !isLoading &&
+    !chartAlert &&
+    !errorMessage &&
+    chartIsStale &&
+    ensureIsArray(queriesResponse).length === 0
+  ) {
+    return (
+      <EmptyState
+        size="large"
+        title={t('Your chart is ready to go!')}
+        description={
+          <span>
+            {t(
+              'Click on "Create chart" button in the control panel on the left to preview a visualization or',
+            )}{' '}
+            <span role="button" tabIndex={0} onClick={onQuery}>
+              {t('click here')}
+            </span>
+            .
+          </span>
+        }
+        image="chart.svg"
+      />
+    );
+  }
+
+  return (
+    <ErrorBoundary onError={handleRenderContainerFailure} showMessage={false}>
+      <Styles
+        data-ui-anchor="chart"
+        className="chart-container"
+        data-test="chart-container"
+        height={height}
+        width={width}
+      >
+        {showSpinner ? renderSpinner(databaseName) : renderChartContainer()}
+      </Styles>
+    </ErrorBoundary>
+  );
 }
 export default Chart;
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
index 5d5022b09c3..92a2b8c52db 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
@@ -82,7 +82,7 @@ const mockActions: MockActions = {
   ) => Dispatch,
 };
 
-const requiredProps: Partial<ChartRendererProps> = {
+const requiredProps: ChartRendererProps = {
   chartId: 1,
   datasource: {} as ChartRendererProps['datasource'],
   formData: {
@@ -111,17 +111,14 @@ afterAll(() => {
 
 test('should render SuperChart', () => {
   const { getByTestId } = render(
-    <ChartRenderer
-      {...(requiredProps as ChartRendererProps)}
-      chartIsStale={false}
-    />,
+    <ChartRenderer {...requiredProps} chartIsStale={false} />,
   );
   expect(getByTestId('mock-super-chart')).toBeInTheDocument();
 });
 
 test('should use latestQueryFormData instead of formData when chartIsStale is true', () => {
   const { getByTestId } = render(
-    <ChartRenderer {...(requiredProps as ChartRendererProps)} chartIsStale />,
+    <ChartRenderer {...requiredProps} chartIsStale />,
   );
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
     JSON.stringify({
@@ -131,9 +128,7 @@ test('should use latestQueryFormData instead of formData when chartIsStale is tr
 });
 
 test('should render chart context menu', () => {
-  const { getByTestId } = render(
-    <ChartRenderer {...(requiredProps as ChartRendererProps)} />,
-  );
+  const { getByTestId } = render(<ChartRenderer {...requiredProps} />);
   expect(getByTestId('mock-chart-context-menu')).toBeInTheDocument();
 });
 
@@ -148,16 +143,13 @@ test('should not render chart context menu if the context menu is suppressed for
     }),
   );
   const { queryByTestId } = render(
-    <ChartRenderer
-      {...(requiredProps as ChartRendererProps)}
-      vizType="chart_without_context_menu"
-    />,
+    <ChartRenderer {...requiredProps} vizType="chart_without_context_menu" />,
   );
   expect(queryByTestId('mock-chart-context-menu')).not.toBeInTheDocument();
 });
 
 test('should detect changes in matrixify properties', () => {
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     formData: {
       ...requiredProps.formData,
@@ -173,41 +165,34 @@ test('should detect changes in matrixify properties', () => {
     chartStatus: 'success',
   };
 
-  render(<ChartRenderer {...(initialProps as ChartRendererProps)} />);
+  const { getByTestId } = render(<ChartRenderer {...initialProps} />);
 
-  // Since we can't directly test shouldComponentUpdate, we verify the component
-  // correctly identifies matrixify-related properties by checking the implementation
-  expect((initialProps.formData as JsonObject).matrixify_mode_rows).toBe(
-    'metrics',
+  // Verify matrixify-related formData is forwarded through to the chart
+  expect(getByTestId('mock-super-chart')).toHaveTextContent(
+    JSON.stringify(initialProps.formData),
   );
-  expect((initialProps.formData as JsonObject).matrixify_dimension_x).toEqual({
-    dimension: 'country',
-    values: ['USA'],
-  });
 });
 
 test('should detect changes in postTransformProps', () => {
   const postTransformProps = jest.fn((x: JsonObject) => x);
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     queriesResponse: [{ data: 'initial' } as unknown as JsonObject],
     chartStatus: 'success',
   };
-  const { rerender } = render(
-    <ChartRenderer {...(initialProps as ChartRendererProps)} />,
-  );
-  const updatedProps: Partial<ChartRendererProps> = {
+  const { rerender } = render(<ChartRenderer {...initialProps} />);
+  const updatedProps: ChartRendererProps = {
     ...initialProps,
     postTransformProps,
   };
   expect(postTransformProps).toHaveBeenCalledTimes(0);
-  rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+  rerender(<ChartRenderer {...updatedProps} />);
   expect(postTransformProps).toHaveBeenCalledTimes(1);
 });
 
 test('should identify matrixify property changes correctly', () => {
   // Test that formData with different matrixify properties triggers updates
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     formData: {
       datasource: '',
@@ -221,16 +206,14 @@ test('should identify matrixify property changes correctly', () => {
     chartStatus: 'success',
   };
 
-  const { rerender, getByTestId } = render(
-    <ChartRenderer {...(initialProps as ChartRendererProps)} />,
-  );
+  const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
 
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
     JSON.stringify(initialProps.formData),
   );
 
   // Update with changed matrixify_dimension_x values
-  const updatedProps: Partial<ChartRendererProps> = {
+  const updatedProps: ChartRendererProps = {
     ...initialProps,
     formData: {
       datasource: '',
@@ -245,7 +228,7 @@ test('should identify matrixify property changes correctly', () => {
     },
   };
 
-  rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+  rerender(<ChartRenderer {...updatedProps} />);
 
   // Verify the component re-rendered with new props
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -254,7 +237,7 @@ test('should identify matrixify property changes correctly', () => {
 });
 
 test('should handle matrixify-related form data changes', () => {
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     formData: {
       datasource: '',
@@ -265,16 +248,14 @@ test('should handle matrixify-related form data changes', () => {
     chartStatus: 'success',
   };
 
-  const { rerender, getByTestId } = render(
-    <ChartRenderer {...(initialProps as ChartRendererProps)} />,
-  );
+  const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
 
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
     JSON.stringify(initialProps.formData),
   );
 
   // Enable matrixify
-  const updatedProps: Partial<ChartRendererProps> = {
+  const updatedProps: ChartRendererProps = {
     ...initialProps,
     formData: {
       datasource: '',
@@ -285,7 +266,7 @@ test('should handle matrixify-related form data changes', () => {
     },
   };
 
-  rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+  rerender(<ChartRenderer {...updatedProps} />);
 
   // Verify the component re-rendered with matrixify enabled
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -294,7 +275,7 @@ test('should handle matrixify-related form data changes', () => {
 });
 
 test('should detect matrixify property addition', () => {
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     formData: {
       datasource: '',
@@ -307,16 +288,14 @@ test('should detect matrixify property addition', () => {
     chartStatus: 'success',
   };
 
-  const { rerender, getByTestId } = render(
-    <ChartRenderer {...(initialProps as ChartRendererProps)} />,
-  );
+  const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
 
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
     JSON.stringify(initialProps.formData),
   );
 
   // Add matrixify_dimension_x
-  const updatedProps: Partial<ChartRendererProps> = {
+  const updatedProps: ChartRendererProps = {
     ...initialProps,
     formData: {
       datasource: '',
@@ -327,7 +306,7 @@ test('should detect matrixify property addition', () => {
     },
   };
 
-  rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+  rerender(<ChartRenderer {...updatedProps} />);
 
   // Verify the component re-rendered with the new property
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
@@ -336,7 +315,7 @@ test('should detect matrixify property addition', () => {
 });
 
 test('should detect nested matrixify property changes', () => {
-  const initialProps: Partial<ChartRendererProps> = {
+  const initialProps: ChartRendererProps = {
     ...requiredProps,
     formData: {
       datasource: '',
@@ -353,16 +332,14 @@ test('should detect nested matrixify property changes', () => {
     chartStatus: 'success',
   };
 
-  const { rerender, getByTestId } = render(
-    <ChartRenderer {...(initialProps as ChartRendererProps)} />,
-  );
+  const { rerender, getByTestId } = render(<ChartRenderer {...initialProps} />);
 
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
     JSON.stringify(initialProps.formData),
   );
 
   // Change nested topN value
-  const updatedProps: Partial<ChartRendererProps> = {
+  const updatedProps: ChartRendererProps = {
     ...initialProps,
     formData: {
       datasource: '',
@@ -377,7 +354,7 @@ test('should detect nested matrixify property changes', () => {
     },
   };
 
-  rerender(<ChartRenderer {...(updatedProps as ChartRendererProps)} />);
+  rerender(<ChartRenderer {...updatedProps} />);
 
   // Verify the component re-rendered with the nested change
   expect(getByTestId('mock-super-chart')).toHaveTextContent(
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx
index b5fa33c04c1..90a35d75a45 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.tsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx
@@ -16,8 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { snakeCase, isEqual, cloneDeep } from 'lodash';
-import { createRef, Component, RefObject, MouseEvent, ReactNode } from 'react';
+import { snakeCase, cloneDeep } from 'lodash';
+import {
+  useCallback,
+  useEffect,
+  useState,
+  useRef,
+  useMemo,
+  MouseEvent,
+  ReactNode,
+  memo,
+} from 'react';
 import {
   SuperChart,
   Behavior,
@@ -28,6 +37,7 @@ import {
   QueryFormData,
   AnnotationData,
   DataMask,
+  FilterState,
   QueryData,
   JsonObject,
   LatestQueryFormData,
@@ -37,6 +47,7 @@ import {
 } from '@superset-ui/core';
 import { logging } from '@apache-superset/core/utils';
 import { t } from '@apache-superset/core/translation';
+import { useTheme } from '@apache-superset/core/theme';
 import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
 import { EmptyState } from '@superset-ui/core/components';
 import { ChartSource } from 'src/types/ChartSource';
@@ -91,12 +102,6 @@ interface OwnState {
   [key: string]: unknown;
 }
 
-// Types for filter state
-interface FilterState {
-  value?: FilterValue[];
-  [key: string]: unknown;
-}
-
 // Props interface
 export interface ChartRendererProps {
   annotationData?: AnnotationData;
@@ -124,7 +129,6 @@ export interface ChartRendererProps {
     merge?: boolean,
     refresh?: boolean,
   ) => void;
-  setDataMask?: (dataMask: DataMask) => void;
   onFilterMenuOpen?: (chartId: number, column: string) => void;
   onFilterMenuClose?: (chartId: number, column: string) => void;
   ownState?: OwnState;
@@ -137,14 +141,6 @@ export interface ChartRendererProps {
   suppressLoadingSpinner?: boolean;
 }
 
-// State interface
-interface ChartRendererState {
-  showContextMenu: boolean;
-  inContextMenu: boolean;
-  legendState: LegendState | undefined;
-  legendIndex: number;
-}
-
 // Hooks interface
 interface ChartHooks {
   onAddFilter: (
@@ -175,402 +171,376 @@ const BIG_NO_RESULT_MIN_HEIGHT = 220;
 
 const behaviors = [Behavior.InteractiveChart];
 
-const defaultProps: Partial<ChartRendererProps> = {
-  addFilter: () => BLANK,
-  onFilterMenuOpen: () => BLANK,
-  onFilterMenuClose: () => BLANK,
-  initialValues: BLANK,
-  setControlValue: () => {},
-  triggerRender: false,
-};
-
-class ChartRenderer extends Component<ChartRendererProps, ChartRendererState> {
-  static defaultProps = defaultProps;
-
-  private hasQueryResponseChange: boolean;
-
-  private contextMenuRef: RefObject<ChartContextMenuRef>;
-
-  private hooks: ChartHooks;
-
-  private mutableQueriesResponse: QueryData[] | null | undefined;
-
-  private renderStartTime: number;
-
-  constructor(props: ChartRendererProps) {
-    super(props);
-    const suppressContextMenu = getChartMetadataRegistry().get(
-      props.formData.viz_type ?? props.vizType,
-    )?.suppressContextMenu;
-    this.state = {
-      showContextMenu:
-        props.source === ChartSource.Dashboard &&
-        !suppressContextMenu &&
-        isFeatureEnabled(FeatureFlag.DrillToDetail),
-      inContextMenu: false,
-      legendState: undefined,
-      legendIndex: 0,
-    };
-    this.hasQueryResponseChange = false;
-    this.renderStartTime = 0;
-
-    this.contextMenuRef = createRef<ChartContextMenuRef>();
-
-    this.handleAddFilter = this.handleAddFilter.bind(this);
-    this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
-    this.handleRenderFailure = this.handleRenderFailure.bind(this);
-    this.handleSetControlValue = this.handleSetControlValue.bind(this);
-    this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
-    this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
-    this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
-    this.handleLegendStateChanged = this.handleLegendStateChanged.bind(this);
-    this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
-    this.handleLegendScroll = this.handleLegendScroll.bind(this);
-
-    this.hooks = {
-      onAddFilter: this.handleAddFilter,
-      onContextMenu: this.state.showContextMenu
-        ? this.handleOnContextMenu
-        : undefined,
-      onError: this.handleRenderFailure,
-      setControlValue: this.handleSetControlValue,
-      onFilterMenuOpen: this.props.onFilterMenuOpen,
-      onFilterMenuClose: this.props.onFilterMenuClose,
-      onLegendStateChanged: this.handleLegendStateChanged,
-      setDataMask: (dataMask: DataMask) => {
-        this.props.actions?.updateDataMask?.(this.props.chartId, dataMask);
-      },
-      onLegendScroll: this.handleLegendScroll,
-      onChartStateChange: this.props.onChartStateChange,
-    };
-
-    // TODO: queriesResponse comes from Redux store but it's being edited by
-    // the plugins, hence we need to clone it to avoid state mutation
-    // until we change the reducers to use Redux Toolkit with Immer
-    this.mutableQueriesResponse = cloneDeep(this.props.queriesResponse);
-  }
-
-  shouldComponentUpdate(
-    nextProps: ChartRendererProps,
-    nextState: ChartRendererState,
-  ): boolean {
-    const resultsReady =
-      nextProps.queriesResponse &&
-      ['success', 'rendered'].indexOf(nextProps.chartStatus as string) > -1 &&
-      !nextProps.queriesResponse?.[0]?.error;
-
-    if (resultsReady) {
-      if (!isEqual(this.state, nextState)) {
-        return true;
-      }
-      this.hasQueryResponseChange =
-        nextProps.queriesResponse !== this.props.queriesResponse;
-
-      if (this.hasQueryResponseChange) {
-        this.mutableQueriesResponse = cloneDeep(nextProps.queriesResponse);
-      }
-
-      // Check if any matrixify-related properties have changed
-      const hasMatrixifyChanges = (): boolean => {
-        const nextFormData = nextProps.formData as JsonObject;
-        const currentFormData = this.props.formData as JsonObject;
-        const isMatrixifyEnabled =
-          nextFormData.matrixify_enable === true &&
-          ((nextFormData.matrixify_mode_rows !== undefined &&
-            nextFormData.matrixify_mode_rows !== 'disabled') ||
-            (nextFormData.matrixify_mode_columns !== undefined &&
-              nextFormData.matrixify_mode_columns !== 'disabled'));
-        if (!isMatrixifyEnabled) return false;
-
-        // Check all matrixify-related properties
-        const matrixifyKeys = Object.keys(nextFormData).filter(key =>
-          key.startsWith('matrixify_'),
-        );
-
-        return matrixifyKeys.some(
-          key => !isEqual(nextFormData[key], currentFormData[key]),
-        );
-      };
-
-      const nextFormData = nextProps.formData as JsonObject;
-      const currentFormData = this.props.formData as JsonObject;
-
-      return (
-        this.hasQueryResponseChange ||
-        !isEqual(nextProps.datasource, this.props.datasource) ||
-        nextProps.annotationData !== this.props.annotationData ||
-        nextProps.ownState !== this.props.ownState ||
-        nextProps.filterState !== this.props.filterState ||
-        nextProps.height !== this.props.height ||
-        nextProps.width !== this.props.width ||
-        nextProps.triggerRender === true ||
-        nextProps.labelsColor !== this.props.labelsColor ||
-        nextProps.labelsColorMap !== this.props.labelsColorMap ||
-        nextFormData.color_scheme !== currentFormData.color_scheme ||
-        nextFormData.stack !== currentFormData.stack ||
-        nextFormData.subcategories !== currentFormData.subcategories ||
-        nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
-        nextProps.emitCrossFilters !== this.props.emitCrossFilters ||
-        nextProps.postTransformProps !== this.props.postTransformProps ||
-        hasMatrixifyChanges()
-      );
-    }
-    return false;
-  }
+interface ChartRendererState {
+  inContextMenu: boolean;
+  legendState: LegendState | undefined;
+  legendIndex: number;
+}
 
-  handleAddFilter(
-    col: string,
-    vals: FilterValue[],
-    merge = true,
-    refresh = true,
-  ): void {
-    this.props.addFilter?.(col, vals, merge, refresh);
+function ChartRendererComponent({
+  addFilter = () => BLANK,
+  onFilterMenuOpen = () => BLANK,
+  onFilterMenuClose = () => BLANK,
+  initialValues = BLANK,
+  setControlValue = () => {},
+  triggerRender = false,
+  ...restProps
+}: ChartRendererProps): JSX.Element | null {
+  const {
+    annotationData,
+    actions,
+    chartId,
+    datasource,
+    formData,
+    latestQueryFormData,
+    height,
+    width,
+    vizType: propVizType,
+    chartAlert,
+    chartStatus,
+    queriesResponse,
+    chartIsStale,
+    ownState,
+    filterState,
+    postTransformProps,
+    source,
+    emitCrossFilters,
+    onChartStateChange,
+  } = restProps;
+
+  const theme = useTheme();
+
+  const suppressContextMenu = getChartMetadataRegistry().get(
+    formData.viz_type ?? propVizType,
+  )?.suppressContextMenu;
+
+  // Derived from props/feature-flags: must NOT live in state, otherwise a
+  // `source` or viz-type change on the same mounted instance would leave
+  // it stale. (Pre-refactor this was a class-instance field recomputed on
+  // every render — preserve that semantic by using a memo here.)
+  const showContextMenu = useMemo(
+    () =>
+      source === ChartSource.Dashboard &&
+      !suppressContextMenu &&
+      isFeatureEnabled(FeatureFlag.DrillToDetail),
+    [source, suppressContextMenu],
+  );
+
+  const [state, setState] = useState<ChartRendererState>({
+    inContextMenu: false,
+    legendState: undefined,
+    legendIndex: 0,
+  });
+
+  const hasQueryResponseChangeRef = useRef(false);
+  const renderStartTimeRef = useRef(0);
+  const contextMenuRef = useRef<ChartContextMenuRef>(null);
+
+  // Results are "ready" when we have a non-error queriesResponse and the
+  // chartStatus reflects it. This mirrors the gating logic from the former
+  // shouldComponentUpdate implementation.
+  const resultsReady =
+    queriesResponse &&
+    ['success', 'rendered'].indexOf(chartStatus as string) > -1 &&
+    !queriesResponse?.[0]?.error;
+
+  // Track whether queriesResponse changed since the previous render so that
+  // handleRenderSuccess / handleRenderFailure know whether to log render time.
+  // Updating a ref during render is safe when the value doesn't affect the
+  // render output (here it's read asynchronously from SuperChart callbacks).
+  const prevQueriesResponseRef = useRef<QueryData[] | null | undefined>(
+    queriesResponse,
+  );
+  if (resultsReady) {
+    hasQueryResponseChangeRef.current =
+      queriesResponse !== prevQueriesResponseRef.current;
   }
+  useEffect(() => {
+    prevQueriesResponseRef.current = queriesResponse;
+  }, [queriesResponse]);
+
+  // Clone queriesResponse to protect against plugin mutation of Redux state.
+  // Gate on `resultsReady` so the deep clone doesn't run for every
+  // queriesResponse identity change during loading/idle (only when results
+  // are actually about to render). Matches the pre-refactor gating.
+  // TODO: remove once reducers use Redux Toolkit with Immer.
+  const mutableQueriesResponse = useMemo(
+    () => (resultsReady ? cloneDeep(queriesResponse) : undefined),
+    [queriesResponse, resultsReady],
+  );
+
+  // Handler functions
+  const handleAddFilter = useCallback(
+    (col: string, vals: FilterValue[], merge = true, refresh = true): void => {
+      addFilter?.(col, vals, merge, refresh);
+    },
+    [addFilter],
+  );
 
-  handleRenderSuccess(): void {
-    const { actions, chartStatus, chartId, vizType } = this.props;
+  const handleRenderSuccess = useCallback((): void => {
     if (['loading', 'rendered'].indexOf(chartStatus as string) < 0) {
       actions.chartRenderingSucceeded(chartId);
     }
 
     // only log chart render time which is triggered by query results change
-    // currently we don't log chart re-render time, like window resize etc
-    if (this.hasQueryResponseChange) {
+    if (hasQueryResponseChangeRef.current) {
       actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
         slice_id: chartId,
-        viz_type: vizType,
-        start_offset: this.renderStartTime,
+        viz_type: propVizType,
+        start_offset: renderStartTimeRef.current,
         ts: new Date().getTime(),
-        duration: Logger.getTimestamp() - this.renderStartTime,
+        duration: Logger.getTimestamp() - renderStartTimeRef.current,
       });
     }
-  }
+  }, [actions, chartId, chartStatus, propVizType]);
+
+  const handleRenderFailure = useCallback(
+    (error: Error, info: { componentStack: string } | null): void => {
+      logging.warn(error);
+      actions.chartRenderingFailed(
+        error.toString(),
+        chartId,
+        info ? info.componentStack : null,
+      );
 
-  handleRenderFailure(
-    error: Error,
-    info: { componentStack: string } | null,
-  ): void {
-    const { actions, chartId } = this.props;
-    logging.warn(error);
-    actions.chartRenderingFailed(
-      error.toString(),
-      chartId,
-      info ? info.componentStack : null,
-    );
+      // only trigger render log when query is changed
+      if (hasQueryResponseChangeRef.current) {
+        actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
+          slice_id: chartId,
+          has_err: true,
+          error_details: error.toString(),
+          start_offset: renderStartTimeRef.current,
+          ts: new Date().getTime(),
+          duration: Logger.getTimestamp() - renderStartTimeRef.current,
+        });
+      }
+    },
+    [actions, chartId],
+  );
 
-    // only trigger render log when query is changed
-    if (this.hasQueryResponseChange) {
-      actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
-        slice_id: chartId,
-        has_err: true,
-        error_details: error.toString(),
-        start_offset: this.renderStartTime,
-        ts: new Date().getTime(),
-        duration: Logger.getTimestamp() - this.renderStartTime,
-      });
-    }
-  }
+  const handleSetControlValue = useCallback(
+    (name: string, value: unknown): void => {
+      if (setControlValue) {
+        setControlValue(name, value);
+      }
+    },
+    [setControlValue],
+  );
 
-  handleSetControlValue(name: string, value: unknown): void {
-    const { setControlValue } = this.props;
-    if (setControlValue) {
-      setControlValue(name, value);
-    }
-  }
+  const handleOnContextMenu = useCallback(
+    (offsetX: number, offsetY: number, filters?: ContextMenuFilters): void => {
+      contextMenuRef.current?.open(offsetX, offsetY, filters);
+      setState(prev => ({ ...prev, inContextMenu: true }));
+    },
+    [contextMenuRef],
+  );
 
-  handleOnContextMenu(
-    offsetX: number,
-    offsetY: number,
-    filters?: ContextMenuFilters,
-  ): void {
-    this.contextMenuRef.current?.open(offsetX, offsetY, filters);
-    this.setState({ inContextMenu: true });
-  }
+  const handleContextMenuSelected = useCallback((): void => {
+    setState(prev => ({ ...prev, inContextMenu: false }));
+  }, []);
 
-  handleContextMenuSelected(): void {
-    this.setState({ inContextMenu: false });
-  }
+  const handleContextMenuClosed = useCallback((): void => {
+    setState(prev => ({ ...prev, inContextMenu: false }));
+  }, []);
 
-  handleContextMenuClosed(): void {
-    this.setState({ inContextMenu: false });
-  }
+  const handleLegendStateChanged = useCallback(
+    (legendState: LegendState): void => {
+      setState(prev => ({ ...prev, legendState }));
+    },
+    [],
+  );
 
-  handleLegendStateChanged(legendState: LegendState): void {
-    this.setState({ legendState });
-  }
+  const handleLegendScroll = useCallback((legendIndex: number): void => {
+    setState(prev => ({ ...prev, legendIndex }));
+  }, []);
 
   // When viz plugins don't handle `contextmenu` event, fallback handler
   // calls `handleOnContextMenu` with no `filters` param.
-  onContextMenuFallback(event: MouseEvent<HTMLDivElement>): void {
-    if (!this.state.inContextMenu) {
-      event.preventDefault();
-      this.handleOnContextMenu(event.clientX, event.clientY);
-    }
-  }
+  const onContextMenuFallback = useCallback(
+    (event: MouseEvent<HTMLDivElement>): void => {
+      if (!state.inContextMenu) {
+        event.preventDefault();
+        handleOnContextMenu(event.clientX, event.clientY);
+      }
+    },
+    [handleOnContextMenu, state.inContextMenu],
+  );
 
-  handleLegendScroll(legendIndex: number): void {
-    this.setState({ legendIndex });
+  const setDataMaskCallback = useCallback(
+    (dataMask: DataMask) => {
+      actions?.updateDataMask?.(chartId, dataMask);
+    },
+    [actions, chartId],
+  );
+
+  // Hooks object - memoized
+  const hooks = useMemo<ChartHooks>(
+    () => ({
+      onAddFilter: handleAddFilter,
+      onContextMenu: showContextMenu ? handleOnContextMenu : undefined,
+      onError: handleRenderFailure,
+      setControlValue: handleSetControlValue,
+      onFilterMenuOpen,
+      onFilterMenuClose,
+      onLegendStateChanged: handleLegendStateChanged,
+      setDataMask: setDataMaskCallback,
+      onLegendScroll: handleLegendScroll,
+      onChartStateChange,
+    }),
+    [
+      handleAddFilter,
+      handleLegendScroll,
+      handleLegendStateChanged,
+      handleOnContextMenu,
+      handleRenderFailure,
+      handleSetControlValue,
+      onChartStateChange,
+      onFilterMenuClose,
+      onFilterMenuOpen,
+      setDataMaskCallback,
+      showContextMenu,
+    ],
+  );
+
+  const hasAnyErrors = queriesResponse?.some(item => item?.error);
+  const hasValidPreviousData =
+    (queriesResponse?.length ?? 0) > 0 && !hasAnyErrors;
+
+  if (!!chartAlert || chartStatus === null) {
+    return null;
   }
 
-  render(): ReactNode {
-    const { chartAlert, chartStatus, chartId, emitCrossFilters } = this.props;
-
-    const hasAnyErrors = this.props.queriesResponse?.some(item => item?.error);
-    const hasValidPreviousData =
-      (this.props.queriesResponse?.length ?? 0) > 0 && !hasAnyErrors;
-
-    if (!!chartAlert || chartStatus === null) {
+  if (chartStatus === 'loading') {
+    if (!restProps.suppressLoadingSpinner || !hasValidPreviousData) {
       return null;
     }
+  }
 
-    if (chartStatus === 'loading') {
-      if (!this.props.suppressLoadingSpinner || !hasValidPreviousData) {
-        return null;
-      }
-    }
-
-    this.renderStartTime = Logger.getTimestamp();
-
-    const {
-      width,
-      height,
-      datasource,
-      annotationData,
-      initialValues,
-      ownState,
-      filterState,
-      chartIsStale,
-      formData,
-      latestQueryFormData,
-      postTransformProps,
-    } = this.props;
-
-    const currentFormData =
-      chartIsStale && latestQueryFormData ? latestQueryFormData : formData;
-    const vizType = currentFormData.viz_type || this.props.vizType;
-
-    // It's bad practice to use unprefixed `vizType` as classnames for chart
-    // container. It may cause css conflicts as in the case of legacy table chart.
-    // When migrating charts, we should gradually add a `superset-chart-` prefix
-    // to each one of them.
-    const snakeCaseVizType = snakeCase(vizType);
-    const chartClassName =
-      vizType === VizType.Table
-        ? `superset-chart-${snakeCaseVizType}`
-        : snakeCaseVizType;
-
-    const webpackHash =
-      process.env.WEBPACK_MODE === 'development'
-        ? `-${
-            // eslint-disable-next-line camelcase
-            typeof __webpack_require__ !== 'undefined' &&
-            // eslint-disable-next-line camelcase, no-undef
-            typeof __webpack_require__.h === 'function' &&
-            // eslint-disable-next-line no-undef, camelcase
-            __webpack_require__.h()
-          }`
-        : '';
-
-    let noResultsComponent: ReactNode;
-    const noResultTitle = t('No results were returned for this query');
-    const noResultDescription =
-      this.props.source === ChartSource.Explore
-        ? t(
-            'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
-          )
-        : undefined;
-    const noResultImage = 'chart.svg';
-    if (
-      (width ?? 0) > BIG_NO_RESULT_MIN_WIDTH &&
-      (height ?? 0) > BIG_NO_RESULT_MIN_HEIGHT
-    ) {
-      noResultsComponent = (
-        <EmptyState
-          size="large"
-          title={noResultTitle}
-          description={noResultDescription}
-          image={noResultImage}
-        />
-      );
-    } else {
-      noResultsComponent = (
-        <EmptyState title={noResultTitle} image={noResultImage} size="small" />
-      );
-    }
-
-    // Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to
-    // Detail props or if it'll cause side-effects (e.g. excessive re-renders).
-    const drillToDetailProps = getChartMetadataRegistry()
-      .get(vizType)
-      ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
-      ? { inContextMenu: this.state.inContextMenu }
-      : {};
-    // By pass no result component when server pagination is enabled & the table has:
-    // - a backend search query, OR
-    // - non-empty AG Grid filter model
-    const hasSearchText = (ownState?.searchText?.length || 0) > 0;
-    const hasAgGridFilters =
-      ownState?.agGridFilterModel &&
-      Object.keys(ownState.agGridFilterModel).length > 0;
-
-    const currentFormDataExtended = currentFormData as JsonObject;
-    const bypassNoResult = !(
-      currentFormDataExtended?.server_pagination &&
-      (hasSearchText || hasAgGridFilters)
+  renderStartTimeRef.current = Logger.getTimestamp();
+
+  const currentFormData =
+    chartIsStale && latestQueryFormData ? latestQueryFormData : formData;
+  const vizType = currentFormData.viz_type || propVizType;
+
+  // It's bad practice to use unprefixed `vizType` as classnames for chart
+  // container. It may cause css conflicts as in the case of legacy table chart.
+  // When migrating charts, we should gradually add a `superset-chart-` prefix
+  // to each one of them.
+  const snakeCaseVizType = snakeCase(vizType);
+  const chartClassName =
+    vizType === VizType.Table
+      ? `superset-chart-${snakeCaseVizType}`
+      : snakeCaseVizType;
+
+  const webpackHash =
+    process.env.WEBPACK_MODE === 'development'
+      ? `-${
+          // eslint-disable-next-line camelcase
+          typeof __webpack_require__ !== 'undefined' &&
+          // eslint-disable-next-line camelcase, no-undef
+          typeof __webpack_require__.h === 'function' &&
+          // eslint-disable-next-line no-undef, camelcase
+          __webpack_require__.h()
+        }`
+      : '';
+
+  let noResultsComponent: ReactNode;
+  const noResultTitle = t('No results were returned for this query');
+  const noResultDescription =
+    source === ChartSource.Explore
+      ? t(
+          'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
+        )
+      : undefined;
+  const noResultImage = 'chart.svg';
+  if (
+    (width ?? 0) > BIG_NO_RESULT_MIN_WIDTH &&
+    (height ?? 0) > BIG_NO_RESULT_MIN_HEIGHT
+  ) {
+    noResultsComponent = (
+      <EmptyState
+        size="large"
+        title={noResultTitle}
+        description={noResultDescription}
+        image={noResultImage}
+      />
     );
-
-    return (
-      <>
-        {this.state.showContextMenu && (
-          <ChartContextMenu
-            ref={this.contextMenuRef}
-            id={chartId}
-            formData={currentFormData as QueryFormData}
-            onSelection={this.handleContextMenuSelected}
-            onClose={this.handleContextMenuClosed}
-          />
-        )}
-        <div
-          onContextMenu={
-            this.state.showContextMenu ? this.onContextMenuFallback : undefined
-          }
-        >
-          <SuperChart
-            disableErrorBoundary
-            key={`${chartId}${webpackHash}`}
-            id={`chart-id-${chartId}`}
-            className={chartClassName}
-            chartType={vizType}
-            width={width}
-            height={height}
-            annotationData={annotationData}
-            datasource={datasource}
-            initialValues={initialValues}
-            formData={currentFormData}
-            ownState={ownState}
-            filterState={filterState}
-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            hooks={this.hooks as any}
-            behaviors={behaviors}
-            queriesData={this.mutableQueriesResponse ?? undefined}
-            onRenderSuccess={this.handleRenderSuccess}
-            onRenderFailure={this.handleRenderFailure}
-            noResults={noResultsComponent}
-            postTransformProps={postTransformProps}
-            emitCrossFilters={emitCrossFilters}
-            legendState={this.state.legendState}
-            enableNoResults={bypassNoResult}
-            legendIndex={this.state.legendIndex}
-            isRefreshing={
-              Boolean(this.props.suppressLoadingSpinner) &&
-              chartStatus === 'loading'
-            }
-            {...drillToDetailProps}
-          />
-        </div>
-      </>
+  } else {
+    noResultsComponent = (
+      <EmptyState title={noResultTitle} image={noResultImage} size="small" />
     );
   }
+
+  // Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to
+  // Detail props or if it'll cause side-effects (e.g. excessive re-renders).
+  const drillToDetailProps = getChartMetadataRegistry()
+    .get(vizType)
+    ?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
+    ? { inContextMenu: state.inContextMenu }
+    : {};
+  // By pass no result component when server pagination is enabled & the table has:
+  // - a backend search query, OR
+  // - non-empty AG Grid filter model
+  const hasSearchText = (ownState?.searchText?.length || 0) > 0;
+  const hasAgGridFilters =
+    ownState?.agGridFilterModel &&
+    Object.keys(ownState.agGridFilterModel).length > 0;
+
+  const currentFormDataExtended = currentFormData as JsonObject;
+  const bypassNoResult = !(
+    currentFormDataExtended?.server_pagination &&
+    (hasSearchText || hasAgGridFilters)
+  );
+
+  return (
+    <>
+      {showContextMenu && (
+        <ChartContextMenu
+          ref={contextMenuRef}
+          id={chartId}
+          formData={currentFormData as QueryFormData}
+          onSelection={handleContextMenuSelected}
+          onClose={handleContextMenuClosed}
+        />
+      )}
+      <div onContextMenu={showContextMenu ? onContextMenuFallback : undefined}>
+        <SuperChart
+          disableErrorBoundary
+          key={`${chartId}${webpackHash}`}
+          id={`chart-id-${chartId}`}
+          className={chartClassName}
+          chartType={vizType}
+          width={width}
+          height={height}
+          theme={theme}
+          annotationData={annotationData}
+          datasource={datasource}
+          initialValues={initialValues}
+          formData={currentFormData}
+          ownState={ownState}
+          filterState={filterState}
+          hooks={hooks as unknown as Parameters<typeof SuperChart>[0]['hooks']}
+          behaviors={behaviors}
+          queriesData={mutableQueriesResponse ?? undefined}
+          onRenderSuccess={handleRenderSuccess}
+          onRenderFailure={handleRenderFailure}
+          noResults={noResultsComponent}
+          postTransformProps={postTransformProps}
+          emitCrossFilters={emitCrossFilters}
+          legendState={state.legendState}
+          enableNoResults={bypassNoResult}
+          legendIndex={state.legendIndex}
+          isRefreshing={
+            Boolean(restProps.suppressLoadingSpinner) &&
+            chartStatus === 'loading'
+          }
+          {...drillToDetailProps}
+        />
+      </div>
+    </>
+  );
 }
 
+const ChartRenderer = memo(ChartRendererComponent);
+
 export default ChartRenderer;
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
index b977433e853..db1670f924b 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByChart.tsx
@@ -23,7 +23,7 @@ import {
   SuperChart,
   ContextMenuFilters,
 } from '@superset-ui/core';
-import { css } from '@apache-superset/core/theme';
+import { css, useTheme } from '@apache-superset/core/theme';
 import { Dataset } from '../types';
 
 interface DrillByChartProps {
@@ -45,6 +45,7 @@ export default function DrillByChart({
   onContextMenu,
   inContextMenu,
 }: DrillByChartProps) {
+  const theme = useTheme();
   const hooks = useMemo(() => ({ onContextMenu }), [onContextMenu]);
 
   return (
@@ -67,6 +68,7 @@ export default function DrillByChart({
         inContextMenu={inContextMenu}
         height="100%"
         width="100%"
+        theme={theme}
       />
     </div>
   );