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

kgabryje 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 eab888c63a perf: Optimize dashboard chart-related components (#31241)
eab888c63a is described below

commit eab888c63a3a6a68c1ea7ec24d12bdf55ab0751b
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Mon Dec 2 15:04:29 2024 +0100

    perf: Optimize dashboard chart-related components (#31241)
---
 .../src/dashboard/components/SliceHeader/index.tsx | 342 ++++----
 .../components/URLShortLinkButton/index.tsx        |  13 +-
 .../dashboard/components/gridComponents/Chart.jsx  | 907 +++++++++++----------
 .../components/gridComponents/Chart.test.jsx       | 112 ++-
 .../components/gridComponents/ChartHolder.tsx      | 225 ++---
 .../src/dashboard/containers/Chart.jsx             | 135 ---
 6 files changed, 850 insertions(+), 884 deletions(-)

diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx 
b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index b281ff320d..fb84bf398f 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -16,7 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { FC, ReactNode, useContext, useEffect, useRef, useState } from 'react';
+import {
+  forwardRef,
+  ReactNode,
+  useContext,
+  useEffect,
+  useRef,
+  useState,
+} from 'react';
 import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
 import { useUiConfig } from 'src/components/UiConfigContext';
 import { Tooltip } from 'src/components/Tooltip';
@@ -34,7 +41,6 @@ import { DashboardPageIdContext } from 
'src/dashboard/containers/DashboardPage';
 const extensionsRegistry = getExtensionsRegistry();
 
 type SliceHeaderProps = SliceHeaderControlsProps & {
-  innerRef?: string;
   updateSliceName?: (arg0: string) => void;
   editMode?: boolean;
   annotationQuery?: object;
@@ -122,176 +128,182 @@ const ChartHeaderStyles = styled.div`
   `}
 `;
 
-const SliceHeader: FC<SliceHeaderProps> = ({
-  innerRef = null,
-  forceRefresh = () => ({}),
-  updateSliceName = () => ({}),
-  toggleExpandSlice = () => ({}),
-  logExploreChart = () => ({}),
-  logEvent,
-  exportCSV = () => ({}),
-  exportXLSX = () => ({}),
-  editMode = false,
-  annotationQuery = {},
-  annotationError = {},
-  cachedDttm = null,
-  updatedDttm = null,
-  isCached = [],
-  isExpanded = false,
-  sliceName = '',
-  supersetCanExplore = false,
-  supersetCanShare = false,
-  supersetCanCSV = false,
-  exportPivotCSV,
-  exportFullCSV,
-  exportFullXLSX,
-  slice,
-  componentId,
-  dashboardId,
-  addSuccessToast,
-  addDangerToast,
-  handleToggleFullSize,
-  isFullSize,
-  chartStatus,
-  formData,
-  width,
-  height,
-}) => {
-  const SliceHeaderExtension = 
extensionsRegistry.get('dashboard.slice.header');
-  const uiConfig = useUiConfig();
-  const dashboardPageId = useContext(DashboardPageIdContext);
-  const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
-  const headerRef = useRef<HTMLDivElement>(null);
-  // TODO: change to indicator field after it will be implemented
-  const crossFilterValue = useSelector<RootState, any>(
-    state => state.dataMask[slice?.slice_id]?.filterState?.value,
-  );
-  const isCrossFiltersEnabled = useSelector<RootState, boolean>(
-    ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
-  );
+const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
+  (
+    {
+      forceRefresh = () => ({}),
+      updateSliceName = () => ({}),
+      toggleExpandSlice = () => ({}),
+      logExploreChart = () => ({}),
+      logEvent,
+      exportCSV = () => ({}),
+      exportXLSX = () => ({}),
+      editMode = false,
+      annotationQuery = {},
+      annotationError = {},
+      cachedDttm = null,
+      updatedDttm = null,
+      isCached = [],
+      isExpanded = false,
+      sliceName = '',
+      supersetCanExplore = false,
+      supersetCanShare = false,
+      supersetCanCSV = false,
+      exportPivotCSV,
+      exportFullCSV,
+      exportFullXLSX,
+      slice,
+      componentId,
+      dashboardId,
+      addSuccessToast,
+      addDangerToast,
+      handleToggleFullSize,
+      isFullSize,
+      chartStatus,
+      formData,
+      width,
+      height,
+    },
+    ref,
+  ) => {
+    const SliceHeaderExtension = extensionsRegistry.get(
+      'dashboard.slice.header',
+    );
+    const uiConfig = useUiConfig();
+    const dashboardPageId = useContext(DashboardPageIdContext);
+    const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
+    const headerRef = useRef<HTMLDivElement>(null);
+    // TODO: change to indicator field after it will be implemented
+    const crossFilterValue = useSelector<RootState, any>(
+      state => state.dataMask[slice?.slice_id]?.filterState?.value,
+    );
+    const isCrossFiltersEnabled = useSelector<RootState, boolean>(
+      ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
+    );
 
-  const canExplore = !editMode && supersetCanExplore;
+    const canExplore = !editMode && supersetCanExplore;
 
-  useEffect(() => {
-    const headerElement = headerRef.current;
-    if (canExplore) {
-      setHeaderTooltip(getSliceHeaderTooltip(sliceName));
-    } else if (
-      headerElement &&
-      (headerElement.scrollWidth > headerElement.offsetWidth ||
-        headerElement.scrollHeight > headerElement.offsetHeight)
-    ) {
-      setHeaderTooltip(sliceName ?? null);
-    } else {
-      setHeaderTooltip(null);
-    }
-  }, [sliceName, width, height, canExplore]);
+    useEffect(() => {
+      const headerElement = headerRef.current;
+      if (canExplore) {
+        setHeaderTooltip(getSliceHeaderTooltip(sliceName));
+      } else if (
+        headerElement &&
+        (headerElement.scrollWidth > headerElement.offsetWidth ||
+          headerElement.scrollHeight > headerElement.offsetHeight)
+      ) {
+        setHeaderTooltip(sliceName ?? null);
+      } else {
+        setHeaderTooltip(null);
+      }
+    }, [sliceName, width, height, canExplore]);
 
-  const exploreUrl = 
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
+    const exploreUrl = 
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
 
-  return (
-    <ChartHeaderStyles data-test="slice-header" ref={innerRef}>
-      <div className="header-title" ref={headerRef}>
-        <Tooltip title={headerTooltip}>
-          <EditableTitle
-            title={
-              sliceName ||
-              (editMode
-                ? '---' // this makes an empty title clickable
-                : '')
-            }
-            canEdit={editMode}
-            onSaveTitle={updateSliceName}
-            showTooltip={false}
-            url={canExplore ? exploreUrl : undefined}
-          />
-        </Tooltip>
-        {!!Object.values(annotationQuery).length && (
-          <Tooltip
-            id="annotations-loading-tooltip"
-            placement="top"
-            title={annotationsLoading}
-          >
-            <i
-              role="img"
-              aria-label={annotationsLoading}
-              className="fa fa-refresh warning"
+    return (
+      <ChartHeaderStyles data-test="slice-header" ref={ref}>
+        <div className="header-title" ref={headerRef}>
+          <Tooltip title={headerTooltip}>
+            <EditableTitle
+              title={
+                sliceName ||
+                (editMode
+                  ? '---' // this makes an empty title clickable
+                  : '')
+              }
+              canEdit={editMode}
+              onSaveTitle={updateSliceName}
+              showTooltip={false}
+              url={canExplore ? exploreUrl : undefined}
             />
           </Tooltip>
-        )}
-        {!!Object.values(annotationError).length && (
-          <Tooltip
-            id="annotation-errors-tooltip"
-            placement="top"
-            title={annotationsError}
-          >
-            <i
-              role="img"
-              aria-label={annotationsError}
-              className="fa fa-exclamation-circle danger"
-            />
-          </Tooltip>
-        )}
-      </div>
-      <div className="header-controls">
-        {!editMode && (
-          <>
-            {SliceHeaderExtension && (
-              <SliceHeaderExtension
-                sliceId={slice.slice_id}
-                dashboardId={dashboardId}
+          {!!Object.values(annotationQuery).length && (
+            <Tooltip
+              id="annotations-loading-tooltip"
+              placement="top"
+              title={annotationsLoading}
+            >
+              <i
+                role="img"
+                aria-label={annotationsLoading}
+                className="fa fa-refresh warning"
               />
-            )}
-            {crossFilterValue && (
-              <Tooltip
-                placement="top"
-                title={t(
-                  'This chart applies cross-filters to charts whose datasets 
contain columns with the same name.',
-                )}
-              >
-                <CrossFilterIcon iconSize="m" />
-              </Tooltip>
-            )}
-            {!uiConfig.hideChartControls && (
-              <FiltersBadge chartId={slice.slice_id} />
-            )}
-            {!uiConfig.hideChartControls && (
-              <SliceHeaderControls
-                slice={slice}
-                isCached={isCached}
-                isExpanded={isExpanded}
-                cachedDttm={cachedDttm}
-                updatedDttm={updatedDttm}
-                toggleExpandSlice={toggleExpandSlice}
-                forceRefresh={forceRefresh}
-                logExploreChart={logExploreChart}
-                logEvent={logEvent}
-                exportCSV={exportCSV}
-                exportPivotCSV={exportPivotCSV}
-                exportFullCSV={exportFullCSV}
-                exportXLSX={exportXLSX}
-                exportFullXLSX={exportFullXLSX}
-                supersetCanExplore={supersetCanExplore}
-                supersetCanShare={supersetCanShare}
-                supersetCanCSV={supersetCanCSV}
-                componentId={componentId}
-                dashboardId={dashboardId}
-                addSuccessToast={addSuccessToast}
-                addDangerToast={addDangerToast}
-                handleToggleFullSize={handleToggleFullSize}
-                isFullSize={isFullSize}
-                isDescriptionExpanded={isExpanded}
-                chartStatus={chartStatus}
-                formData={formData}
-                exploreUrl={exploreUrl}
-                crossFiltersEnabled={isCrossFiltersEnabled}
+            </Tooltip>
+          )}
+          {!!Object.values(annotationError).length && (
+            <Tooltip
+              id="annotation-errors-tooltip"
+              placement="top"
+              title={annotationsError}
+            >
+              <i
+                role="img"
+                aria-label={annotationsError}
+                className="fa fa-exclamation-circle danger"
               />
-            )}
-          </>
-        )}
-      </div>
-    </ChartHeaderStyles>
-  );
-};
+            </Tooltip>
+          )}
+        </div>
+        <div className="header-controls">
+          {!editMode && (
+            <>
+              {SliceHeaderExtension && (
+                <SliceHeaderExtension
+                  sliceId={slice.slice_id}
+                  dashboardId={dashboardId}
+                />
+              )}
+              {crossFilterValue && (
+                <Tooltip
+                  placement="top"
+                  title={t(
+                    'This chart applies cross-filters to charts whose datasets 
contain columns with the same name.',
+                  )}
+                >
+                  <CrossFilterIcon iconSize="m" />
+                </Tooltip>
+              )}
+              {!uiConfig.hideChartControls && (
+                <FiltersBadge chartId={slice.slice_id} />
+              )}
+              {!uiConfig.hideChartControls && (
+                <SliceHeaderControls
+                  slice={slice}
+                  isCached={isCached}
+                  isExpanded={isExpanded}
+                  cachedDttm={cachedDttm}
+                  updatedDttm={updatedDttm}
+                  toggleExpandSlice={toggleExpandSlice}
+                  forceRefresh={forceRefresh}
+                  logExploreChart={logExploreChart}
+                  logEvent={logEvent}
+                  exportCSV={exportCSV}
+                  exportPivotCSV={exportPivotCSV}
+                  exportFullCSV={exportFullCSV}
+                  exportXLSX={exportXLSX}
+                  exportFullXLSX={exportFullXLSX}
+                  supersetCanExplore={supersetCanExplore}
+                  supersetCanShare={supersetCanShare}
+                  supersetCanCSV={supersetCanCSV}
+                  componentId={componentId}
+                  dashboardId={dashboardId}
+                  addSuccessToast={addSuccessToast}
+                  addDangerToast={addDangerToast}
+                  handleToggleFullSize={handleToggleFullSize}
+                  isFullSize={isFullSize}
+                  isDescriptionExpanded={isExpanded}
+                  chartStatus={chartStatus}
+                  formData={formData}
+                  exploreUrl={exploreUrl}
+                  crossFiltersEnabled={isCrossFiltersEnabled}
+                />
+              )}
+            </>
+          )}
+        </div>
+      </ChartHeaderStyles>
+    );
+  },
+);
 
 export default SliceHeader;
diff --git 
a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx 
b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
index 849b1cd444..f227a9e845 100644
--- a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
+++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
@@ -22,7 +22,7 @@ import Popover, { PopoverProps } from 
'src/components/Popover';
 import CopyToClipboard from 'src/components/CopyToClipboard';
 import { getDashboardPermalink } from 'src/utils/urlUtils';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { useSelector } from 'react-redux';
+import { shallowEqual, useSelector } from 'react-redux';
 import { RootState } from 'src/dashboard/types';
 
 export type URLShortLinkButtonProps = {
@@ -42,10 +42,13 @@ export default function URLShortLinkButton({
 }: URLShortLinkButtonProps) {
   const [shortUrl, setShortUrl] = useState('');
   const { addDangerToast } = useToasts();
-  const { dataMask, activeTabs } = useSelector((state: RootState) => ({
-    dataMask: state.dataMask,
-    activeTabs: state.dashboardState.activeTabs,
-  }));
+  const { dataMask, activeTabs } = useSelector(
+    (state: RootState) => ({
+      dataMask: state.dataMask,
+      activeTabs: state.dashboardState.activeTabs,
+    }),
+    shallowEqual,
+  );
 
   const getCopyUrl = async () => {
     try {
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx 
b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
index d125ccb626..5cc8ba6d37 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx
@@ -17,11 +17,13 @@
  * under the License.
  */
 import cx from 'classnames';
-import { Component } from 'react';
+import { useCallback, useEffect, useRef, useMemo, useState, memo } from 
'react';
 import PropTypes from 'prop-types';
 import { styled, t, logging } from '@superset-ui/core';
-import { debounce, isEqual } from 'lodash';
-import { withRouter } from 'react-router-dom';
+import { debounce } from 'lodash';
+import { useHistory } from 'react-router-dom';
+import { bindActionCreators } from 'redux';
+import { useDispatch, useSelector } from 'react-redux';
 
 import { exportChart, mountExploreUrl } from 'src/explore/exploreUtils';
 import ChartContainer from 'src/components/Chart/ChartContainer';
@@ -32,13 +34,30 @@ import {
   LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART,
   LOG_ACTIONS_FORCE_REFRESH_CHART,
 } from 'src/logger/LogUtils';
-import { areObjectsEqual } from 'src/reduxUtils';
 import { postFormData } from 'src/explore/exploreUtils/formData';
 import { URL_PARAMS } from 'src/constants';
+import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
 
 import SliceHeader from '../SliceHeader';
 import MissingChart from '../MissingChart';
-import { slicePropShape, chartPropShape } from '../../util/propShapes';
+import {
+  addDangerToast,
+  addSuccessToast,
+} from '../../../components/MessageToasts/actions';
+import {
+  setFocusedFilterField,
+  toggleExpandSlice,
+  unsetFocusedFilterField,
+} from '../../actions/dashboardState';
+import { changeFilter } from '../../actions/dashboardFilters';
+import { refreshChart } from '../../../components/Chart/chartAction';
+import { logEvent } from '../../../logger/actions';
+import {
+  getActiveFilters,
+  getAppliedFilterValues,
+} from '../../util/activeDashboardFilters';
+import getFormDataWithExtraFilters from 
'../../util/charts/getFormDataWithExtraFilters';
+import { PLACEHOLDER_DATASOURCE } from '../../constants';
 
 const propTypes = {
   id: PropTypes.number.isRequired,
@@ -50,53 +69,15 @@ const propTypes = {
   isComponentVisible: PropTypes.bool,
   handleToggleFullSize: PropTypes.func.isRequired,
   setControlValue: PropTypes.func,
-
-  // from redux
-  chart: chartPropShape.isRequired,
-  formData: PropTypes.object.isRequired,
-  labelsColor: PropTypes.object,
-  labelsColorMap: PropTypes.object,
-  datasource: PropTypes.object,
-  slice: slicePropShape.isRequired,
   sliceName: PropTypes.string.isRequired,
-  timeout: PropTypes.number.isRequired,
-  maxRows: PropTypes.number.isRequired,
-  // all active filter fields in dashboard
-  filters: PropTypes.object.isRequired,
-  refreshChart: PropTypes.func.isRequired,
-  logEvent: PropTypes.func.isRequired,
-  toggleExpandSlice: PropTypes.func.isRequired,
-  changeFilter: PropTypes.func.isRequired,
-  setFocusedFilterField: PropTypes.func.isRequired,
-  unsetFocusedFilterField: PropTypes.func.isRequired,
-  editMode: PropTypes.bool.isRequired,
-  isExpanded: PropTypes.bool.isRequired,
-  isCached: PropTypes.bool,
-  supersetCanExplore: PropTypes.bool.isRequired,
-  supersetCanShare: PropTypes.bool.isRequired,
-  supersetCanCSV: PropTypes.bool.isRequired,
-  addSuccessToast: PropTypes.func.isRequired,
-  addDangerToast: PropTypes.func.isRequired,
-  ownState: PropTypes.object,
-  filterState: PropTypes.object,
-  postTransformProps: PropTypes.func,
-  datasetsStatus: PropTypes.oneOf(['loading', 'error', 'complete']),
+  isFullSize: PropTypes.bool,
+  extraControls: PropTypes.object,
   isInView: PropTypes.bool,
-  emitCrossFilters: PropTypes.bool,
-};
-
-const defaultProps = {
-  isCached: false,
-  isComponentVisible: true,
 };
 
 // we use state + shouldComponentUpdate() logic to prevent perf-wrecking
 // resizing across all slices on a dashboard on every update
 const RESIZE_TIMEOUT = 500;
-const SHOULD_UPDATE_ON_PROP_CHANGES = Object.keys(propTypes).filter(
-  prop =>
-    prop !== 'width' && prop !== 'height' && prop !== 'isComponentVisible',
-);
 const DEFAULT_HEADER_HEIGHT = 22;
 
 const ChartWrapper = styled.div`
@@ -121,429 +102,457 @@ const SliceContainer = styled.div`
   max-height: 100%;
 `;
 
-class Chart extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      width: props.width,
-      height: props.height,
-      descriptionHeight: 0,
-    };
-
-    this.changeFilter = this.changeFilter.bind(this);
-    this.handleFilterMenuOpen = this.handleFilterMenuOpen.bind(this);
-    this.handleFilterMenuClose = this.handleFilterMenuClose.bind(this);
-    this.exportCSV = this.exportCSV.bind(this);
-    this.exportPivotCSV = this.exportPivotCSV.bind(this);
-    this.exportFullCSV = this.exportFullCSV.bind(this);
-    this.exportXLSX = this.exportXLSX.bind(this);
-    this.exportFullXLSX = this.exportFullXLSX.bind(this);
-    this.forceRefresh = this.forceRefresh.bind(this);
-    this.resize = debounce(this.resize.bind(this), RESIZE_TIMEOUT);
-    this.setDescriptionRef = this.setDescriptionRef.bind(this);
-    this.setHeaderRef = this.setHeaderRef.bind(this);
-    this.getChartHeight = this.getChartHeight.bind(this);
-    this.getDescriptionHeight = this.getDescriptionHeight.bind(this);
-  }
-
-  shouldComponentUpdate(nextProps, nextState) {
-    // this logic mostly pertains to chart resizing. we keep a copy of the 
dimensions in
-    // state so that we can buffer component size updates and only update on 
the final call
-    // which improves performance significantly
-    if (
-      nextState.width !== this.state.width ||
-      nextState.height !== this.state.height ||
-      nextState.descriptionHeight !== this.state.descriptionHeight ||
-      !isEqual(nextProps.datasource, this.props.datasource)
-    ) {
-      return true;
-    }
-
-    // allow chart to update if the status changed and the previous status was 
loading.
-    if (
-      this.props?.chart?.chartStatus !== nextProps?.chart?.chartStatus &&
-      this.props?.chart?.chartStatus === 'loading'
-    ) {
-      return true;
-    }
-
-    // allow chart update/re-render only if visible:
-    // under selected tab or no tab layout
-    if (nextProps.isComponentVisible) {
-      if (nextProps.chart.triggerQuery) {
-        return true;
-      }
-
-      if (nextProps.isFullSize !== this.props.isFullSize) {
-        this.resize();
-        return false;
-      }
-
-      if (
-        nextProps.width !== this.props.width ||
-        nextProps.height !== this.props.height ||
-        nextProps.width !== this.state.width ||
-        nextProps.height !== this.state.height
-      ) {
-        this.resize();
-      }
-
-      for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) {
-        const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i];
-        // use deep objects equality comparison to prevent
-        // unnecessary updates when objects references change
-        if (!areObjectsEqual(nextProps[prop], this.props[prop])) {
-          return true;
-        }
-      }
-    } else if (
-      // chart should re-render if color scheme or label colors were changed
-      nextProps.formData?.color_scheme !== this.props.formData?.color_scheme ||
-      !areObjectsEqual(
-        nextProps.formData?.label_colors || {},
-        this.props.formData?.label_colors || {},
-      ) ||
-      !areObjectsEqual(
-        nextProps.formData?.map_label_colors || {},
-        this.props.formData?.map_label_colors || {},
-      ) ||
-      !isEqual(
-        nextProps.formData?.shared_label_colors || [],
-        this.props.formData?.shared_label_colors || [],
-      )
-    ) {
-      return true;
-    }
-
-    // `cacheBusterProp` is injected by react-hot-loader
-    return this.props.cacheBusterProp !== nextProps.cacheBusterProp;
-  }
-
-  componentDidMount() {
-    if (this.props.isExpanded) {
-      const descriptionHeight = this.getDescriptionHeight();
-      this.setState({ descriptionHeight });
-    }
-  }
-
-  componentWillUnmount() {
-    this.resize.cancel();
-  }
-
-  componentDidUpdate(prevProps) {
-    if (this.props.isExpanded !== prevProps.isExpanded) {
-      const descriptionHeight = this.getDescriptionHeight();
-      // eslint-disable-next-line react/no-did-update-set-state
-      this.setState({ descriptionHeight });
+const EMPTY_OBJECT = {};
+
+const Chart = props => {
+  const dispatch = useDispatch();
+  const descriptionRef = useRef(null);
+  const headerRef = useRef(null);
+
+  const boundActionCreators = useMemo(
+    () =>
+      bindActionCreators(
+        {
+          addSuccessToast,
+          addDangerToast,
+          toggleExpandSlice,
+          changeFilter,
+          setFocusedFilterField,
+          unsetFocusedFilterField,
+          refreshChart,
+          logEvent,
+        },
+        dispatch,
+      ),
+    [dispatch],
+  );
+
+  const chart = useSelector(state => state.charts[props.id] || EMPTY_OBJECT);
+  const slice = useSelector(
+    state => state.sliceEntities.slices[props.id] || EMPTY_OBJECT,
+  );
+  const editMode = useSelector(state => state.dashboardState.editMode);
+  const isExpanded = useSelector(
+    state => !!state.dashboardState.expandedSlices[props.id],
+  );
+  const supersetCanExplore = useSelector(
+    state => !!state.dashboardInfo.superset_can_explore,
+  );
+  const supersetCanShare = useSelector(
+    state => !!state.dashboardInfo.superset_can_share,
+  );
+  const supersetCanCSV = useSelector(
+    state => !!state.dashboardInfo.superset_can_csv,
+  );
+  const timeout = useSelector(
+    state => state.dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
+  );
+  const emitCrossFilters = useSelector(
+    state => !!state.dashboardInfo.crossFiltersEnabled,
+  );
+  const datasource = useSelector(
+    state =>
+      (chart &&
+        chart.form_data &&
+        state.datasources[chart.form_data.datasource]) ||
+      PLACEHOLDER_DATASOURCE,
+  );
+
+  const [descriptionHeight, setDescriptionHeight] = useState(0);
+  const [height, setHeight] = useState(props.height);
+  const [width, setWidth] = useState(props.width);
+  const history = useHistory();
+  const resize = useCallback(
+    debounce(() => {
+      const { width, height } = props;
+      setHeight(height);
+      setWidth(width);
+    }, RESIZE_TIMEOUT),
+    [props.width, props.height],
+  );
+
+  const ownColorScheme = chart.form_data?.color_scheme;
+
+  const addFilter = useCallback(
+    (newSelectedValues = {}) => {
+      boundActionCreators.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
+        id: chart.id,
+        columns: Object.keys(newSelectedValues).filter(
+          key => newSelectedValues[key] !== null,
+        ),
+      });
+      boundActionCreators.changeFilter(chart.id, newSelectedValues);
+    },
+    [boundActionCreators.logEvent, boundActionCreators.changeFilter, chart.id],
+  );
+
+  useEffect(() => {
+    if (isExpanded) {
+      const descriptionHeight =
+        isExpanded && descriptionRef.current
+          ? descriptionRef.current?.offsetHeight
+          : 0;
+      setDescriptionHeight(descriptionHeight);
     }
-  }
-
-  getDescriptionHeight() {
-    return this.props.isExpanded && this.descriptionRef
-      ? this.descriptionRef.offsetHeight
-      : 0;
-  }
-
-  getChartHeight() {
-    const headerHeight = this.getHeaderHeight();
-    return Math.max(
-      this.state.height - headerHeight - this.state.descriptionHeight,
-      20,
-    );
-  }
-
-  getHeaderHeight() {
-    if (this.headerRef) {
-      const computedStyle = getComputedStyle(this.headerRef).getPropertyValue(
-        'margin-bottom',
-      );
+  }, [isExpanded]);
+
+  useEffect(
+    () => () => {
+      resize.cancel();
+    },
+    [resize],
+  );
+
+  useEffect(() => {
+    resize();
+  }, [resize, props.isFullSize]);
+
+  const getHeaderHeight = useCallback(() => {
+    if (headerRef.current) {
+      const computedStyle = getComputedStyle(
+        headerRef.current,
+      ).getPropertyValue('margin-bottom');
       const marginBottom = parseInt(computedStyle, 10) || 0;
-      return this.headerRef.offsetHeight + marginBottom;
+      return headerRef.current.offsetHeight + marginBottom;
     }
     return DEFAULT_HEADER_HEIGHT;
-  }
-
-  setDescriptionRef(ref) {
-    this.descriptionRef = ref;
-  }
-
-  setHeaderRef(ref) {
-    this.headerRef = ref;
-  }
-
-  resize() {
-    const { width, height } = this.props;
-    this.setState(() => ({ width, height }));
-  }
-
-  changeFilter(newSelectedValues = {}) {
-    this.props.logEvent(LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, {
-      id: this.props.chart.id,
-      columns: Object.keys(newSelectedValues).filter(
-        key => newSelectedValues[key] !== null,
-      ),
+  }, [headerRef]);
+
+  const getChartHeight = useCallback(() => {
+    const headerHeight = getHeaderHeight();
+    return Math.max(height - headerHeight - descriptionHeight, 20);
+  }, [getHeaderHeight, height, descriptionHeight]);
+
+  const handleFilterMenuOpen = useCallback(
+    (chartId, column) => {
+      boundActionCreators.setFocusedFilterField(chartId, column);
+    },
+    [boundActionCreators.setFocusedFilterField],
+  );
+
+  const handleFilterMenuClose = useCallback(
+    (chartId, column) => {
+      boundActionCreators.unsetFocusedFilterField(chartId, column);
+    },
+    [boundActionCreators.unsetFocusedFilterField],
+  );
+
+  const logExploreChart = useCallback(() => {
+    boundActionCreators.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
+      slice_id: slice.slice_id,
+      is_cached: props.isCached,
     });
-    this.props.changeFilter(this.props.chart.id, newSelectedValues);
-  }
-
-  handleFilterMenuOpen(chartId, column) {
-    this.props.setFocusedFilterField(chartId, column);
-  }
-
-  handleFilterMenuClose(chartId, column) {
-    this.props.unsetFocusedFilterField(chartId, column);
-  }
-
-  logExploreChart = () => {
-    this.props.logEvent(LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, {
-      slice_id: this.props.slice.slice_id,
-      is_cached: this.props.isCached,
-    });
-  };
-
-  onExploreChart = async clickEvent => {
-    const isOpenInNewTab =
-      clickEvent.shiftKey || clickEvent.ctrlKey || clickEvent.metaKey;
-    try {
-      const lastTabId = window.localStorage.getItem('last_tab_id');
-      const nextTabId = lastTabId
-        ? String(Number.parseInt(lastTabId, 10) + 1)
-        : undefined;
-      const key = await postFormData(
-        this.props.datasource.id,
-        this.props.datasource.type,
-        this.props.formData,
-        this.props.slice.slice_id,
-        nextTabId,
-      );
-      const url = mountExploreUrl(null, {
-        [URL_PARAMS.formDataKey.name]: key,
-        [URL_PARAMS.sliceId.name]: this.props.slice.slice_id,
-      });
-      if (isOpenInNewTab) {
-        window.open(url, '_blank', 'noreferrer');
-      } else {
-        this.props.history.push(url);
-      }
-    } catch (error) {
-      logging.error(error);
-      this.props.addDangerToast(t('An error occurred while opening Explore'));
-    }
-  };
-
-  exportFullCSV() {
-    this.exportCSV(true);
-  }
-
-  exportCSV(isFullCSV = false) {
-    this.exportTable('csv', isFullCSV);
-  }
-
-  exportPivotCSV() {
-    this.exportTable('csv', false, true);
-  }
-
-  exportXLSX() {
-    this.exportTable('xlsx', false);
-  }
-
-  exportFullXLSX() {
-    this.exportTable('xlsx', true);
-  }
-
-  exportTable(format, isFullCSV, isPivot = false) {
-    const logAction =
-      format === 'csv'
-        ? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
-        : LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART;
-    this.props.logEvent(logAction, {
-      slice_id: this.props.slice.slice_id,
-      is_cached: this.props.isCached,
-    });
-    exportChart({
-      formData: isFullCSV
-        ? { ...this.props.formData, row_limit: this.props.maxRows }
-        : this.props.formData,
-      resultType: isPivot ? 'post_processed' : 'full',
-      resultFormat: format,
-      force: true,
-      ownState: this.props.ownState,
-    });
-  }
-
-  forceRefresh() {
-    this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, {
-      slice_id: this.props.slice.slice_id,
-      is_cached: this.props.isCached,
-    });
-    return this.props.refreshChart(
-      this.props.chart.id,
-      true,
-      this.props.dashboardId,
-    );
-  }
-
-  render() {
-    const {
-      id,
-      componentId,
-      dashboardId,
+  }, [boundActionCreators.logEvent, slice.slice_id, props.isCached]);
+
+  const chartConfiguration = useSelector(
+    state => state.dashboardInfo.metadata?.chart_configuration,
+  );
+  const colorScheme = useSelector(state => state.dashboardState.colorScheme);
+  const colorNamespace = useSelector(
+    state => state.dashboardState.colorNamespace,
+  );
+  const datasetsStatus = useSelector(
+    state => state.dashboardState.datasetsStatus,
+  );
+  const allSliceIds = useSelector(state => state.dashboardState.sliceIds);
+  const nativeFilters = useSelector(state => state.nativeFilters?.filters);
+  const dataMask = useSelector(state => state.dataMask);
+  const labelsColor = useSelector(
+    state => state.dashboardInfo?.metadata?.label_colors || EMPTY_OBJECT,
+  );
+  const labelsColorMap = useSelector(
+    state => state.dashboardInfo?.metadata?.map_label_colors || EMPTY_OBJECT,
+  );
+  const sharedLabelsColors = useSelector(state =>
+    enforceSharedLabelsColorsArray(
+      state.dashboardInfo?.metadata?.shared_label_colors,
+    ),
+  );
+
+  const formData = useMemo(
+    () =>
+      getFormDataWithExtraFilters({
+        chart,
+        chartConfiguration,
+        filters: getAppliedFilterValues(props.id),
+        colorScheme,
+        colorNamespace,
+        sliceId: props.id,
+        nativeFilters,
+        allSliceIds,
+        dataMask,
+        extraControls: props.extraControls,
+        labelsColor,
+        labelsColorMap,
+        sharedLabelsColors,
+        ownColorScheme,
+      }),
+    [
       chart,
-      slice,
-      datasource,
-      isExpanded,
-      editMode,
-      filters,
-      formData,
+      chartConfiguration,
+      props.id,
+      props.extraControls,
+      colorScheme,
+      colorNamespace,
+      nativeFilters,
+      allSliceIds,
+      dataMask,
       labelsColor,
       labelsColorMap,
-      updateSliceName,
-      sliceName,
-      toggleExpandSlice,
-      timeout,
-      supersetCanExplore,
-      supersetCanShare,
-      supersetCanCSV,
-      addSuccessToast,
-      addDangerToast,
-      ownState,
-      filterState,
-      handleToggleFullSize,
-      isFullSize,
-      setControlValue,
-      postTransformProps,
-      datasetsStatus,
-      isInView,
-      emitCrossFilters,
-      logEvent,
-    } = this.props;
-
-    const { width } = this.state;
-    // this prevents throwing in the case that a gridComponent
-    // references a chart that is not associated with the dashboard
-    if (!chart || !slice) {
-      return <MissingChart height={this.getChartHeight()} />;
-    }
+      sharedLabelsColors,
+      ownColorScheme,
+    ],
+  );
+
+  const onExploreChart = useCallback(
+    async clickEvent => {
+      const isOpenInNewTab =
+        clickEvent.shiftKey || clickEvent.ctrlKey || clickEvent.metaKey;
+      try {
+        const lastTabId = window.localStorage.getItem('last_tab_id');
+        const nextTabId = lastTabId
+          ? String(Number.parseInt(lastTabId, 10) + 1)
+          : undefined;
+        const key = await postFormData(
+          datasource.id,
+          datasource.type,
+          formData,
+          slice.slice_id,
+          nextTabId,
+        );
+        const url = mountExploreUrl(null, {
+          [URL_PARAMS.formDataKey.name]: key,
+          [URL_PARAMS.sliceId.name]: slice.slice_id,
+        });
+        if (isOpenInNewTab) {
+          window.open(url, '_blank', 'noreferrer');
+        } else {
+          history.push(url);
+        }
+      } catch (error) {
+        logging.error(error);
+        boundActionCreators.addDangerToast(
+          t('An error occurred while opening Explore'),
+        );
+      }
+    },
+    [
+      datasource.id,
+      datasource.type,
+      formData,
+      slice.slice_id,
+      boundActionCreators.addDangerToast,
+      history,
+    ],
+  );
+
+  const exportTable = useCallback(
+    (format, isFullCSV, isPivot = false) => {
+      const logAction =
+        format === 'csv'
+          ? LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART
+          : LOG_ACTIONS_EXPORT_XLSX_DASHBOARD_CHART;
+      boundActionCreators.logEvent(logAction, {
+        slice_id: slice.slice_id,
+        is_cached: props.isCached,
+      });
+      exportChart({
+        formData: isFullCSV
+          ? { ...formData, row_limit: props.maxRows }
+          : formData,
+        resultType: isPivot ? 'post_processed' : 'full',
+        resultFormat: format,
+        force: true,
+        ownState: props.ownState,
+      });
+    },
+    [
+      slice.slice_id,
+      props.isCached,
+      formData,
+      props.maxRows,
+      props.ownState,
+      boundActionCreators.logEvent,
+    ],
+  );
+
+  const exportCSV = useCallback(
+    (isFullCSV = false) => {
+      exportTable('csv', isFullCSV);
+    },
+    [exportTable],
+  );
+
+  const exportFullCSV = useCallback(() => {
+    exportCSV(true);
+  }, [exportCSV]);
+
+  const exportPivotCSV = useCallback(() => {
+    exportTable('csv', false, true);
+  }, [exportTable]);
+
+  const exportXLSX = useCallback(() => {
+    exportTable('xlsx', false);
+  }, [exportTable]);
+
+  const exportFullXLSX = useCallback(() => {
+    exportTable('xlsx', true);
+  }, [exportTable]);
+
+  const forceRefresh = useCallback(() => {
+    boundActionCreators.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, {
+      slice_id: slice.slice_id,
+      is_cached: props.isCached,
+    });
+    return boundActionCreators.refreshChart(chart.id, true, props.dashboardId);
+  }, [
+    boundActionCreators.refreshChart,
+    chart.id,
+    props.dashboardId,
+    slice.slice_id,
+    props.isCached,
+    boundActionCreators.logEvent,
+  ]);
+
+  if (chart === EMPTY_OBJECT || slice === EMPTY_OBJECT) {
+    return <MissingChart height={getChartHeight()} />;
+  }
 
-    const { queriesResponse, chartUpdateEndTime, chartStatus } = chart;
-    const isLoading = chartStatus === 'loading';
+  const { queriesResponse, chartUpdateEndTime, chartStatus, annotationQuery } =
+    chart;
+  const isLoading = chartStatus === 'loading';
+  // eslint-disable-next-line camelcase
+  const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
+  const cachedDttm =
     // eslint-disable-next-line camelcase
-    const isCached = queriesResponse?.map(({ is_cached }) => is_cached) || [];
-    const cachedDttm =
-      // eslint-disable-next-line camelcase
-      queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
-    const initialValues = {};
-
-    return (
-      <SliceContainer
-        className="chart-slice"
-        data-test="chart-grid-component"
-        data-test-chart-id={id}
-        data-test-viz-type={slice.viz_type}
-        data-test-chart-name={slice.slice_name}
-      >
-        <SliceHeader
-          innerRef={this.setHeaderRef}
-          slice={slice}
-          isExpanded={isExpanded}
-          isCached={isCached}
-          cachedDttm={cachedDttm}
-          updatedDttm={chartUpdateEndTime}
-          toggleExpandSlice={toggleExpandSlice}
-          forceRefresh={this.forceRefresh}
-          editMode={editMode}
-          annotationQuery={chart.annotationQuery}
-          logExploreChart={this.logExploreChart}
-          logEvent={logEvent}
-          onExploreChart={this.onExploreChart}
-          exportCSV={this.exportCSV}
-          exportPivotCSV={this.exportPivotCSV}
-          exportXLSX={this.exportXLSX}
-          exportFullCSV={this.exportFullCSV}
-          exportFullXLSX={this.exportFullXLSX}
-          updateSliceName={updateSliceName}
-          sliceName={sliceName}
-          supersetCanExplore={supersetCanExplore}
-          supersetCanShare={supersetCanShare}
-          supersetCanCSV={supersetCanCSV}
-          componentId={componentId}
-          dashboardId={dashboardId}
-          filters={filters}
-          addSuccessToast={addSuccessToast}
-          addDangerToast={addDangerToast}
-          handleToggleFullSize={handleToggleFullSize}
-          isFullSize={isFullSize}
-          chartStatus={chart.chartStatus}
-          formData={formData}
-          width={width}
-          height={this.getHeaderHeight()}
-        />
-
-        {/*
+    queriesResponse?.map(({ cached_dttm }) => cached_dttm) || [];
+
+  return (
+    <SliceContainer
+      className="chart-slice"
+      data-test="chart-grid-component"
+      data-test-chart-id={props.id}
+      data-test-viz-type={slice.viz_type}
+      data-test-chart-name={slice.slice_name}
+    >
+      <SliceHeader
+        ref={headerRef}
+        slice={slice}
+        isExpanded={isExpanded}
+        isCached={isCached}
+        cachedDttm={cachedDttm}
+        updatedDttm={chartUpdateEndTime}
+        toggleExpandSlice={boundActionCreators.toggleExpandSlice}
+        forceRefresh={forceRefresh}
+        editMode={editMode}
+        annotationQuery={annotationQuery}
+        logExploreChart={logExploreChart}
+        logEvent={boundActionCreators.logEvent}
+        onExploreChart={onExploreChart}
+        exportCSV={exportCSV}
+        exportPivotCSV={exportPivotCSV}
+        exportXLSX={exportXLSX}
+        exportFullCSV={exportFullCSV}
+        exportFullXLSX={exportFullXLSX}
+        updateSliceName={props.updateSliceName}
+        sliceName={props.sliceName}
+        supersetCanExplore={supersetCanExplore}
+        supersetCanShare={supersetCanShare}
+        supersetCanCSV={supersetCanCSV}
+        componentId={props.componentId}
+        dashboardId={props.dashboardId}
+        filters={getActiveFilters() || EMPTY_OBJECT}
+        addSuccessToast={boundActionCreators.addSuccessToast}
+        addDangerToast={boundActionCreators.addDangerToast}
+        handleToggleFullSize={props.handleToggleFullSize}
+        isFullSize={props.isFullSize}
+        chartStatus={chartStatus}
+        formData={formData}
+        width={width}
+        height={getHeaderHeight()}
+      />
+
+      {/*
           This usage of dangerouslySetInnerHTML is safe since it is being used 
to render
           markdown that is sanitized with nh3. See:
              https://github.com/apache/superset/pull/4390
           and
              https://github.com/apache/superset/pull/23862
         */}
-        {isExpanded && slice.description_markeddown && (
-          <div
-            className="slice_description bs-callout bs-callout-default"
-            ref={this.setDescriptionRef}
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
-            role="complementary"
+      {isExpanded && slice.description_markeddown && (
+        <div
+          className="slice_description bs-callout bs-callout-default"
+          ref={descriptionRef}
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: slice.description_markeddown }}
+          role="complementary"
+        />
+      )}
+
+      <ChartWrapper
+        className={cx('dashboard-chart')}
+        aria-label={slice.description}
+      >
+        {isLoading && (
+          <ChartOverlay
+            style={{
+              width,
+              height: getChartHeight(),
+            }}
           />
         )}
 
-        <ChartWrapper
-          className={cx('dashboard-chart')}
-          aria-label={slice.description}
-        >
-          {isLoading && (
-            <ChartOverlay
-              style={{
-                width,
-                height: this.getChartHeight(),
-              }}
-            />
-          )}
-
-          <ChartContainer
-            width={width}
-            height={this.getChartHeight()}
-            addFilter={this.changeFilter}
-            onFilterMenuOpen={this.handleFilterMenuOpen}
-            onFilterMenuClose={this.handleFilterMenuClose}
-            annotationData={chart.annotationData}
-            chartAlert={chart.chartAlert}
-            chartId={id}
-            chartStatus={chartStatus}
-            datasource={datasource}
-            dashboardId={dashboardId}
-            initialValues={initialValues}
-            formData={formData}
-            labelsColor={labelsColor}
-            labelsColorMap={labelsColorMap}
-            ownState={ownState}
-            filterState={filterState}
-            queriesResponse={chart.queriesResponse}
-            timeout={timeout}
-            triggerQuery={chart.triggerQuery}
-            vizType={slice.viz_type}
-            setControlValue={setControlValue}
-            postTransformProps={postTransformProps}
-            datasetsStatus={datasetsStatus}
-            isInView={isInView}
-            emitCrossFilters={emitCrossFilters}
-          />
-        </ChartWrapper>
-      </SliceContainer>
-    );
-  }
-}
+        <ChartContainer
+          width={width}
+          height={getChartHeight()}
+          addFilter={addFilter}
+          onFilterMenuOpen={handleFilterMenuOpen}
+          onFilterMenuClose={handleFilterMenuClose}
+          annotationData={chart.annotationData}
+          chartAlert={chart.chartAlert}
+          chartId={props.id}
+          chartStatus={chartStatus}
+          datasource={datasource}
+          dashboardId={props.dashboardId}
+          initialValues={EMPTY_OBJECT}
+          formData={formData}
+          labelsColor={labelsColor}
+          labelsColorMap={labelsColorMap}
+          ownState={dataMask[props.id]?.ownState}
+          filterState={dataMask[props.id]?.filterState}
+          queriesResponse={chart.queriesResponse}
+          timeout={timeout}
+          triggerQuery={chart.triggerQuery}
+          vizType={slice.viz_type}
+          setControlValue={props.setControlValue}
+          datasetsStatus={datasetsStatus}
+          isInView={props.isInView}
+          emitCrossFilters={emitCrossFilters}
+        />
+      </ChartWrapper>
+    </SliceContainer>
+  );
+};
 
 Chart.propTypes = propTypes;
-Chart.defaultProps = defaultProps;
 
-export default withRouter(Chart);
+export default memo(Chart, (prevProps, nextProps) => {
+  if (prevProps.cacheBusterProp !== nextProps.cacheBusterProp) {
+    return false;
+  }
+  return (
+    !nextProps.isComponentVisible ||
+    (prevProps.isInView === nextProps.isInView &&
+      prevProps.componentId === nextProps.componentId &&
+      prevProps.id === nextProps.id &&
+      prevProps.dashboardId === nextProps.dashboardId &&
+      prevProps.extraControls === nextProps.extraControls &&
+      prevProps.handleToggleFullSize === nextProps.handleToggleFullSize &&
+      prevProps.isFullSize === nextProps.isFullSize &&
+      prevProps.setControlValue === nextProps.setControlValue &&
+      prevProps.sliceName === nextProps.sliceName &&
+      prevProps.updateSliceName === nextProps.updateSliceName &&
+      prevProps.width === nextProps.width &&
+      prevProps.height === nextProps.height)
+  );
+});
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx 
b/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
index e54dac7570..223b98d578 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.test.jsx
@@ -18,6 +18,7 @@
  */
 import { fireEvent, render } from 'spec/helpers/testing-library';
 import { FeatureFlag, VizType } from '@superset-ui/core';
+import * as redux from 'redux';
 
 import Chart from 'src/dashboard/components/gridComponents/Chart';
 import * as exploreUtils from 'src/explore/exploreUtils';
@@ -32,18 +33,10 @@ const props = {
   width: 100,
   height: 100,
   updateSliceName() {},
-
   // from redux
   maxRows: 666,
-  chart: chartQueries[queryId],
   formData: chartQueries[queryId].form_data,
   datasource: mockDatasource[sliceEntities.slices[queryId].datasource],
-  slice: {
-    ...sliceEntities.slices[queryId],
-    description_markeddown: 'markdown',
-    owners: [],
-    viz_type: VizType.Table,
-  },
   sliceName: sliceEntities.slices[queryId].slice_name,
   timeout: 60,
   filters: {},
@@ -63,20 +56,60 @@ const props = {
   exportFullXLSX() {},
   componentId: 'test',
   dashboardId: 111,
-  editMode: false,
-  isExpanded: false,
-  supersetCanExplore: false,
-  supersetCanCSV: false,
-  supersetCanShare: false,
 };
 
-function setup(overrideProps) {
-  return render(<Chart.WrappedComponent {...props} {...overrideProps} />, {
+const defaultState = {
+  charts: chartQueries,
+  sliceEntities: {
+    ...sliceEntities,
+    slices: {
+      [queryId]: {
+        ...sliceEntities.slices[queryId],
+        description_markeddown: 'markdown',
+        owners: [],
+        viz_type: VizType.Table,
+      },
+    },
+  },
+  datasources: mockDatasource,
+  dashboardState: { editMode: false, expandedSlices: {} },
+  dashboardInfo: {
+    superset_can_explore: false,
+    superset_can_share: false,
+    superset_can_csv: false,
+    common: { conf: { SUPERSET_WEBSERVER_TIMEOUT: 0 } },
+  },
+};
+
+function setup(overrideProps, overrideState) {
+  return render(<Chart {...props} {...overrideProps} />, {
     useRedux: true,
     useRouter: true,
+    initialState: { ...defaultState, ...overrideState },
   });
 }
 
+const refreshChart = jest.fn();
+const logEvent = jest.fn();
+const changeFilter = jest.fn();
+const addSuccessToast = jest.fn();
+const addDangerToast = jest.fn();
+const toggleExpandSlice = jest.fn();
+const setFocusedFilterField = jest.fn();
+const unsetFocusedFilterField = jest.fn();
+beforeAll(() => {
+  jest.spyOn(redux, 'bindActionCreators').mockImplementation(() => ({
+    refreshChart,
+    logEvent,
+    changeFilter,
+    addSuccessToast,
+    addDangerToast,
+    toggleExpandSlice,
+    setFocusedFilterField,
+    unsetFocusedFilterField,
+  }));
+});
+
 test('should render a SliceHeader', () => {
   const { getByTestId, container } = setup();
   expect(getByTestId('slice-header')).toBeInTheDocument();
@@ -89,23 +122,20 @@ test('should render a ChartContainer', () => {
 });
 
 test('should render a description if it has one and isExpanded=true', () => {
-  const { container } = setup({ isExpanded: true });
-  expect(container.querySelector('.slice_description')).toBeInTheDocument();
-});
-
-test('should calculate the description height if it has one and 
isExpanded=true', () => {
-  const spy = jest.spyOn(
-    Chart.WrappedComponent.prototype,
-    'getDescriptionHeight',
+  const { container } = setup(
+    {},
+    {
+      dashboardState: {
+        ...defaultState.dashboardState,
+        expandedSlices: { [props.id]: true },
+      },
+    },
   );
-  const { container } = setup({ isExpanded: true });
   expect(container.querySelector('.slice_description')).toBeInTheDocument();
-  expect(spy).toHaveBeenCalled();
 });
 
 test('should call refreshChart when SliceHeader calls forceRefresh', () => {
-  const refreshChart = jest.fn();
-  const { getByText, getByRole } = setup({ refreshChart });
+  const { getByText, getByRole } = setup({});
   fireEvent.click(getByRole('button', { name: 'More Options' }));
   fireEvent.click(getByText('Force refresh'));
   expect(refreshChart).toHaveBeenCalled();
@@ -122,7 +152,12 @@ test('should call exportChart when exportCSV is clicked', 
async () => {
   const stubbedExportCSV = jest
     .spyOn(exploreUtils, 'exportChart')
     .mockImplementation(() => {});
-  const { findByText, getByRole } = setup({ supersetCanCSV: true });
+  const { findByText, getByRole } = setup(
+    {},
+    {
+      dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
+    },
+  );
   fireEvent.click(getByRole('button', { name: 'More Options' }));
   fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
   const exportAction = await findByText('Export to .CSV');
@@ -145,7 +180,12 @@ test('should call exportChart with row_limit props.maxRows 
when exportFullCSV is
   const stubbedExportCSV = jest
     .spyOn(exploreUtils, 'exportChart')
     .mockImplementation(() => {});
-  const { findByText, getByRole } = setup({ supersetCanCSV: true });
+  const { findByText, getByRole } = setup(
+    {},
+    {
+      dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
+    },
+  );
   fireEvent.click(getByRole('button', { name: 'More Options' }));
   fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
   const exportAction = await findByText('Export to full .CSV');
@@ -167,7 +207,12 @@ test('should call exportChart when exportXLSX is clicked', 
async () => {
   const stubbedExportXLSX = jest
     .spyOn(exploreUtils, 'exportChart')
     .mockImplementation(() => {});
-  const { findByText, getByRole } = setup({ supersetCanCSV: true });
+  const { findByText, getByRole } = setup(
+    {},
+    {
+      dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
+    },
+  );
   fireEvent.click(getByRole('button', { name: 'More Options' }));
   fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
   const exportAction = await findByText('Export to Excel');
@@ -189,7 +234,12 @@ test('should call exportChart with row_limit props.maxRows 
when exportFullXLSX i
   const stubbedExportXLSX = jest
     .spyOn(exploreUtils, 'exportChart')
     .mockImplementation(() => {});
-  const { findByText, getByRole } = setup({ supersetCanCSV: true });
+  const { findByText, getByRole } = setup(
+    {},
+    {
+      dashboardInfo: { ...defaultState.dashboardInfo, superset_can_csv: true },
+    },
+  );
   fireEvent.click(getByRole('button', { name: 'More Options' }));
   fireEvent.mouseOver(getByRole('button', { name: 'Download right' }));
   const exportAction = await findByText('Export to full Excel');
diff --git 
a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx 
b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
index d31e240e4f..887f1baa73 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState, useMemo, useCallback, useEffect } from 'react';
+import { useState, useMemo, useCallback, useEffect, memo } from 'react';
 
 import { ResizeCallback, ResizeStartCallback } from 're-resizable';
 import cx from 'classnames';
@@ -24,7 +24,7 @@ import { useSelector } from 'react-redux';
 import { css, useTheme } from '@superset-ui/core';
 import { LayoutItem, RootState } from 'src/dashboard/types';
 import AnchorLink from 'src/dashboard/components/AnchorLink';
-import Chart from 'src/dashboard/containers/Chart';
+import Chart from 'src/dashboard/components/gridComponents/Chart';
 import DeleteComponentButton from 
'src/dashboard/components/DeleteComponentButton';
 import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
 import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
@@ -70,7 +70,7 @@ interface ChartHolderProps {
   isInView: boolean;
 }
 
-const ChartHolder: React.FC<ChartHolderProps> = ({
+const ChartHolder = ({
   id,
   parentId,
   component,
@@ -92,7 +92,7 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
   handleComponentDrop,
   setFullSizeChartId,
   isInView,
-}) => {
+}: ChartHolderProps) => {
   const theme = useTheme();
   const fullSizeStyle = css`
     && {
@@ -107,9 +107,13 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
   const isFullSize = fullSizeChartId === chartId;
 
   const focusHighlightStyles = useFilterFocusHighlightStyles(chartId);
-  const dashboardState = useSelector(
-    (state: RootState) => state.dashboardState,
+  const directPathToChild = useSelector(
+    (state: RootState) => state.dashboardState.directPathToChild,
   );
+  const directPathLastUpdated = useSelector(
+    (state: RootState) => state.dashboardState.directPathLastUpdated ?? 0,
+  );
+
   const [extraControls, setExtraControls] = useState<Record<string, unknown>>(
     {},
   );
@@ -118,18 +122,8 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
   const [currentDirectPathLastUpdated, setCurrentDirectPathLastUpdated] =
     useState(0);
 
-  const directPathToChild = useMemo(
-    () => dashboardState?.directPathToChild ?? [],
-    [dashboardState],
-  );
-
-  const directPathLastUpdated = useMemo(
-    () => dashboardState?.directPathLastUpdated ?? 0,
-    [dashboardState],
-  );
-
   const infoFromPath = useMemo(
-    () => getChartAndLabelComponentIdFromPath(directPathToChild) as any,
+    () => getChartAndLabelComponentIdFromPath(directPathToChild ?? []) as any,
     [directPathToChild],
   );
 
@@ -191,26 +185,26 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
   ]);
 
   const { chartWidth, chartHeight } = useMemo(() => {
-    let chartWidth = 0;
-    let chartHeight = 0;
+    let width = 0;
+    let height = 0;
 
     if (isFullSize) {
-      chartWidth = window.innerWidth - CHART_MARGIN;
-      chartHeight = window.innerHeight - CHART_MARGIN;
+      width = window.innerWidth - CHART_MARGIN;
+      height = window.innerHeight - CHART_MARGIN;
     } else {
-      chartWidth = Math.floor(
+      width = Math.floor(
         widthMultiple * columnWidth +
           (widthMultiple - 1) * GRID_GUTTER_SIZE -
           CHART_MARGIN,
       );
-      chartHeight = Math.floor(
+      height = Math.floor(
         component.meta.height * GRID_BASE_UNIT - CHART_MARGIN,
       );
     }
 
     return {
-      chartWidth,
-      chartHeight,
+      chartWidth: width,
+      chartHeight: height,
     };
   }, [columnWidth, component, isFullSize, widthMultiple]);
 
@@ -244,6 +238,111 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
     }));
   }, []);
 
+  const renderChild = useCallback(
+    ({ dragSourceRef }) => (
+      <ResizableContainer
+        id={component.id}
+        adjustableWidth={parentComponent.type === ROW_TYPE}
+        adjustableHeight
+        widthStep={columnWidth}
+        widthMultiple={widthMultiple}
+        heightStep={GRID_BASE_UNIT}
+        heightMultiple={component.meta.height}
+        minWidthMultiple={GRID_MIN_COLUMN_COUNT}
+        minHeightMultiple={GRID_MIN_ROW_UNITS}
+        maxWidthMultiple={availableColumnCount + widthMultiple}
+        onResizeStart={onResizeStart}
+        onResize={onResize}
+        onResizeStop={onResizeStop}
+        editMode={editMode}
+      >
+        <div
+          ref={dragSourceRef}
+          data-test="dashboard-component-chart-holder"
+          style={focusHighlightStyles}
+          css={isFullSize ? fullSizeStyle : undefined}
+          className={cx(
+            'dashboard-component',
+            'dashboard-component-chart-holder',
+            // The following class is added to support custom dashboard 
styling via the CSS editor
+            `dashboard-chart-id-${chartId}`,
+            outlinedComponentId ? 'fade-in' : 'fade-out',
+          )}
+        >
+          {!editMode && (
+            <AnchorLink
+              id={component.id}
+              scrollIntoView={outlinedComponentId === component.id}
+            />
+          )}
+          {!!outlinedComponentId && (
+            <style>
+              {`label[for=${outlinedColumnName}] + .Select .Select__control {
+                    border-color: #00736a;
+                    transition: border-color 1s ease-in-out;
+                  }`}
+            </style>
+          )}
+          <Chart
+            componentId={component.id}
+            id={component.meta.chartId}
+            dashboardId={dashboardId}
+            width={chartWidth}
+            height={chartHeight}
+            sliceName={
+              component.meta.sliceNameOverride || component.meta.sliceName || 
''
+            }
+            updateSliceName={handleUpdateSliceName}
+            isComponentVisible={isComponentVisible}
+            handleToggleFullSize={handleToggleFullSize}
+            isFullSize={isFullSize}
+            setControlValue={handleExtraControl}
+            extraControls={extraControls}
+            isInView={isInView}
+          />
+          {editMode && (
+            <HoverMenu position="top">
+              <div data-test="dashboard-delete-component-button">
+                <DeleteComponentButton onDelete={handleDeleteComponent} />
+              </div>
+            </HoverMenu>
+          )}
+        </div>
+      </ResizableContainer>
+    ),
+    [
+      component.id,
+      component.meta.height,
+      component.meta.chartId,
+      component.meta.sliceNameOverride,
+      component.meta.sliceName,
+      parentComponent.type,
+      columnWidth,
+      widthMultiple,
+      availableColumnCount,
+      onResizeStart,
+      onResize,
+      onResizeStop,
+      editMode,
+      focusHighlightStyles,
+      isFullSize,
+      fullSizeStyle,
+      chartId,
+      outlinedComponentId,
+      outlinedColumnName,
+      dashboardId,
+      chartWidth,
+      chartHeight,
+      handleUpdateSliceName,
+      isComponentVisible,
+      handleToggleFullSize,
+      handleExtraControl,
+      extraControls,
+      isInView,
+      handleDeleteComponent,
+    ],
+  );
+
   return (
     <Draggable
       component={component}
@@ -255,81 +354,9 @@ const ChartHolder: React.FC<ChartHolderProps> = ({
       disableDragDrop={false}
       editMode={editMode}
     >
-      {({ dragSourceRef }) => (
-        <ResizableContainer
-          id={component.id}
-          adjustableWidth={parentComponent.type === ROW_TYPE}
-          adjustableHeight
-          widthStep={columnWidth}
-          widthMultiple={widthMultiple}
-          heightStep={GRID_BASE_UNIT}
-          heightMultiple={component.meta.height}
-          minWidthMultiple={GRID_MIN_COLUMN_COUNT}
-          minHeightMultiple={GRID_MIN_ROW_UNITS}
-          maxWidthMultiple={availableColumnCount + widthMultiple}
-          onResizeStart={onResizeStart}
-          onResize={onResize}
-          onResizeStop={onResizeStop}
-          editMode={editMode}
-        >
-          <div
-            ref={dragSourceRef}
-            data-test="dashboard-component-chart-holder"
-            style={focusHighlightStyles}
-            css={isFullSize ? fullSizeStyle : undefined}
-            className={cx(
-              'dashboard-component',
-              'dashboard-component-chart-holder',
-              // The following class is added to support custom dashboard 
styling via the CSS editor
-              `dashboard-chart-id-${chartId}`,
-              outlinedComponentId ? 'fade-in' : 'fade-out',
-            )}
-          >
-            {!editMode && (
-              <AnchorLink
-                id={component.id}
-                scrollIntoView={outlinedComponentId === component.id}
-              />
-            )}
-            {!!outlinedComponentId && (
-              <style>
-                {`label[for=${outlinedColumnName}] + .Select .Select__control {
-                    border-color: #00736a;
-                    transition: border-color 1s ease-in-out;
-                  }`}
-              </style>
-            )}
-            <Chart
-              componentId={component.id}
-              id={component.meta.chartId}
-              dashboardId={dashboardId}
-              width={chartWidth}
-              height={chartHeight}
-              sliceName={
-                component.meta.sliceNameOverride ||
-                component.meta.sliceName ||
-                ''
-              }
-              updateSliceName={handleUpdateSliceName}
-              isComponentVisible={isComponentVisible}
-              handleToggleFullSize={handleToggleFullSize}
-              isFullSize={isFullSize}
-              setControlValue={handleExtraControl}
-              extraControls={extraControls}
-              isInView={isInView}
-            />
-            {editMode && (
-              <HoverMenu position="top">
-                <div data-test="dashboard-delete-component-button">
-                  <DeleteComponentButton onDelete={handleDeleteComponent} />
-                </div>
-              </HoverMenu>
-            )}
-          </div>
-        </ResizableContainer>
-      )}
+      {renderChild}
     </Draggable>
   );
 };
 
-export default ChartHolder;
+export default memo(ChartHolder);
diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx 
b/superset-frontend/src/dashboard/containers/Chart.jsx
deleted file mode 100644
index 9f00bd0bf7..0000000000
--- a/superset-frontend/src/dashboard/containers/Chart.jsx
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
-import {
-  toggleExpandSlice,
-  setFocusedFilterField,
-  unsetFocusedFilterField,
-} from 'src/dashboard/actions/dashboardState';
-import { updateComponents } from 'src/dashboard/actions/dashboardLayout';
-import { changeFilter } from 'src/dashboard/actions/dashboardFilters';
-import {
-  addSuccessToast,
-  addDangerToast,
-} from 'src/components/MessageToasts/actions';
-import { refreshChart } from 'src/components/Chart/chartAction';
-import { logEvent } from 'src/logger/actions';
-import {
-  getActiveFilters,
-  getAppliedFilterValues,
-} from 'src/dashboard/util/activeDashboardFilters';
-import getFormDataWithExtraFilters from 
'src/dashboard/util/charts/getFormDataWithExtraFilters';
-import Chart from 'src/dashboard/components/gridComponents/Chart';
-import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
-import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
-
-const EMPTY_OBJECT = {};
-
-function mapStateToProps(
-  {
-    charts: chartQueries,
-    dashboardInfo,
-    dashboardState,
-    dataMask,
-    datasources,
-    sliceEntities,
-    nativeFilters,
-    common,
-  },
-  ownProps,
-) {
-  const { id, extraControls, setControlValue } = ownProps;
-  const chart = chartQueries[id] || EMPTY_OBJECT;
-  const datasource =
-    (chart && chart.form_data && datasources[chart.form_data.datasource]) ||
-    PLACEHOLDER_DATASOURCE;
-  const {
-    colorScheme: appliedColorScheme,
-    colorNamespace,
-    datasetsStatus,
-  } = dashboardState;
-  const labelsColor = dashboardInfo?.metadata?.label_colors || {};
-  const labelsColorMap = dashboardInfo?.metadata?.map_label_colors || {};
-  const sharedLabelsColors = enforceSharedLabelsColorsArray(
-    dashboardInfo?.metadata?.shared_label_colors,
-  );
-  const ownColorScheme = chart.form_data?.color_scheme;
-  // note: this method caches filters if possible to prevent render cascades
-  const formData = getFormDataWithExtraFilters({
-    chart,
-    chartConfiguration: dashboardInfo.metadata?.chart_configuration,
-    charts: chartQueries,
-    filters: getAppliedFilterValues(id),
-    colorNamespace,
-    colorScheme: appliedColorScheme,
-    ownColorScheme,
-    sliceId: id,
-    nativeFilters: nativeFilters?.filters,
-    allSliceIds: dashboardState.sliceIds,
-    dataMask,
-    extraControls,
-    labelsColor,
-    labelsColorMap,
-    sharedLabelsColors,
-  });
-
-  formData.dashboardId = dashboardInfo.id;
-
-  return {
-    chart,
-    datasource,
-    labelsColor,
-    labelsColorMap,
-    slice: sliceEntities.slices[id],
-    timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT,
-    filters: getActiveFilters() || EMPTY_OBJECT,
-    formData,
-    editMode: dashboardState.editMode,
-    isExpanded: !!dashboardState.expandedSlices[id],
-    supersetCanExplore: !!dashboardInfo.superset_can_explore,
-    supersetCanShare: !!dashboardInfo.superset_can_share,
-    supersetCanCSV: !!dashboardInfo.superset_can_csv,
-    ownState: dataMask[id]?.ownState,
-    filterState: dataMask[id]?.filterState,
-    maxRows: common.conf.SQL_MAX_ROW,
-    setControlValue,
-    datasetsStatus,
-    emitCrossFilters: !!dashboardInfo.crossFiltersEnabled,
-  };
-}
-
-function mapDispatchToProps(dispatch) {
-  return bindActionCreators(
-    {
-      updateComponents,
-      addSuccessToast,
-      addDangerToast,
-      toggleExpandSlice,
-      changeFilter,
-      setFocusedFilterField,
-      unsetFocusedFilterField,
-      refreshChart,
-      logEvent,
-    },
-    dispatch,
-  );
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(Chart);

Reply via email to