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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6ff96cf  refactor: useListViewResource hook for charts, dashboards, 
datasets (#10680)
6ff96cf is described below

commit 6ff96cfc7202d5ef5ad31ec76bff26daef6bd8fc
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Wed Aug 26 15:39:18 2020 -0700

    refactor: useListViewResource hook for charts, dashboards, datasets (#10680)
---
 .../views/CRUD/chart/ChartList_spec.jsx            |  12 +-
 .../views/CRUD/dashboard/DashboardList_spec.jsx    |  11 +-
 .../src/views/CRUD/chart/ChartList.tsx             | 704 ++++++++----------
 .../src/views/CRUD/dashboard/DashboardList.tsx     | 783 +++++++++------------
 .../src/views/CRUD/data/dataset/DatasetList.tsx    | 735 +++++++++----------
 superset-frontend/src/views/CRUD/hooks.ts          | 224 ++++++
 superset-frontend/src/views/CRUD/types.ts          |  22 +
 superset-frontend/src/views/CRUD/utils.tsx         |  56 +-
 8 files changed, 1216 insertions(+), 1331 deletions(-)

diff --git 
a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx 
b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
index d640116..73dd9ee 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -17,18 +17,18 @@
  * under the License.
  */
 import React from 'react';
-import { mount } from 'enzyme';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { styledMount as mount } from 'spec/helpers/theming';
 
 import ChartList from 'src/views/CRUD/chart/ChartList';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import ListView from 'src/components/ListView';
 import PropertiesModal from 'src/explore/components/PropertiesModal';
 import ListViewCard from 'src/components/ListViewCard';
-
 // store needed for withToasts(ChartTable)
 const mockStore = configureStore([thunk]);
 const store = mockStore({});
@@ -78,8 +78,10 @@ describe('ChartList', () => {
   const mockedProps = {};
   const wrapper = mount(<ChartList {...mockedProps} />, {
     context: { store },
-    wrappingComponent: ThemeProvider,
-    wrappingComponentProps: { theme: supersetTheme },
+  });
+
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
   });
 
   it('renders', () => {
diff --git 
a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
index 7eb634c..fd82414 100644
--- 
a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
+++ 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
@@ -17,11 +17,12 @@
  * under the License.
  */
 import React from 'react';
-import { mount } from 'enzyme';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { styledMount as mount } from 'spec/helpers/theming';
 
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
@@ -69,8 +70,10 @@ describe('DashboardList', () => {
   const mockedProps = {};
   const wrapper = mount(<DashboardList {...mockedProps} />, {
     context: { store },
-    wrappingComponent: ThemeProvider,
-    wrappingComponentProps: { theme: supersetTheme },
+  });
+
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
   });
 
   it('renders', () => {
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx 
b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index fb8d905..514a93a 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -19,15 +19,11 @@
 import { SupersetClient } from '@superset-ui/connection';
 import { t } from '@superset-ui/translation';
 import { getChartMetadataRegistry } from '@superset-ui/chart';
-import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState, useMemo } from 'react';
 import rison from 'rison';
 import { uniqBy } from 'lodash';
-import {
-  createFetchRelated,
-  createErrorHandler,
-  createFaveStarHandlers,
-} from 'src/views/CRUD/utils';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
 import AvatarIcon from 'src/components/AvatarIcon';
@@ -35,7 +31,6 @@ import Icon from 'src/components/Icon';
 import FaveStar from 'src/components/FaveStar';
 import ListView, {
   ListViewProps,
-  FetchDataConfig,
   Filters,
   SelectOption,
 } from 'src/components/ListView';
@@ -49,23 +44,6 @@ import { Dropdown, Menu } from 'src/common/components';
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/slice';
 
-interface Props {
-  addDangerToast: (msg: string) => void;
-  addSuccessToast: (msg: string) => void;
-}
-
-interface State {
-  bulkSelectEnabled: boolean;
-  chartCount: number;
-  charts: Chart[];
-  favoriteStatus: object;
-  lastFetchDataConfig: FetchDataConfig | null;
-  loading: boolean;
-  permissions: string[];
-  // for now we need to use the Slice type defined in PropertiesModal.
-  // In future it would be better to have a unified Chart entity.
-  sliceCurrentlyEditing: Slice | null;
-}
 const createFetchDatasets = (handleError: (err: Response) => void) => async (
   filterValue = '',
   pageIndex?: number,
@@ -102,190 +80,239 @@ const createFetchDatasets = (handleError: (err: 
Response) => void) => async (
   }
   return [];
 };
-class ChartList extends React.PureComponent<Props, State> {
-  static propTypes = {
-    addDangerToast: PropTypes.func.isRequired,
-  };
 
-  state: State = {
-    bulkSelectEnabled: false,
-    chartCount: 0,
-    charts: [],
-    favoriteStatus: {}, // Hash mapping dashboard id to 'isStarred' status
-    lastFetchDataConfig: null,
-    loading: true,
-    permissions: [],
-    sliceCurrentlyEditing: null,
-  };
+interface ChartListProps {
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+}
+
+function ChartList(props: ChartListProps) {
+  const {
+    state: {
+      loading,
+      resourceCount: chartCount,
+      resourceCollection: charts,
+      bulkSelectEnabled,
+    },
+    setResourceCollection: setCharts,
+    hasPerm,
+    fetchData,
+    toggleBulkSelect,
+    refreshData,
+  } = useListViewResource<Chart>('chart', t('chart'), props.addDangerToast);
+  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
+    {},
+    FAVESTAR_BASE_URL,
+    props.addDangerToast,
+  );
+  const [
+    sliceCurrentlyEditing,
+    setSliceCurrentlyEditing,
+  ] = useState<Slice | null>(null);
+
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+  const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+
+  function openChartEditModal(chart: Chart) {
+    setSliceCurrentlyEditing({
+      slice_id: chart.id,
+      slice_name: chart.slice_name,
+      description: chart.description,
+      cache_timeout: chart.cache_timeout,
+    });
+  }
+
+  function closeChartEditModal() {
+    setSliceCurrentlyEditing(null);
+  }
+
+  function handleChartUpdated(edits: Chart) {
+    // update the chart in our state with the edited info
+    const newCharts = charts.map(chart =>
+      chart.id === edits.id ? { ...chart, ...edits } : chart,
+    );
+    setCharts(newCharts);
+  }
+
+  function handleChartDelete({ id, slice_name: sliceName }: Chart) {
+    SupersetClient.delete({
+      endpoint: `/api/v1/chart/${id}`,
+    }).then(
+      () => {
+        refreshData();
+        props.addSuccessToast(t('Deleted: %s', sliceName));
+      },
+      () => {
+        props.addDangerToast(t('There was an issue deleting: %s', sliceName));
+      },
+    );
+  }
 
-  componentDidMount() {
-    SupersetClient.get({
-      endpoint: `/api/v1/chart/_info`,
+  function handleBulkChartDelete(chartsToDelete: Chart[]) {
+    SupersetClient.delete({
+      endpoint: `/api/v1/chart/?q=${rison.encode(
+        chartsToDelete.map(({ id }) => id),
+      )}`,
     }).then(
-      ({ json: infoJson = {} }) => {
-        this.setState({
-          permissions: infoJson.permissions,
-        });
+      ({ json = {} }) => {
+        refreshData();
+        props.addSuccessToast(json.message);
       },
       createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('An error occurred while fetching chart info: %s', errMsg),
+        props.addDangerToast(
+          t('There was an issue deleting the selected charts: %s', errMsg),
         ),
       ),
     );
   }
 
-  get canEdit() {
-    return this.hasPerm('can_edit');
-  }
-
-  get canDelete() {
-    return this.hasPerm('can_delete');
+  function renderFaveStar(id: number) {
+    return (
+      <FaveStar
+        itemId={id}
+        fetchFaveStar={fetchFaveStar}
+        saveFaveStar={saveFaveStar}
+        isStarred={!!favoriteStatusRef.current[id]}
+        height={20}
+        width={20}
+      />
+    );
   }
 
-  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-
-  fetchMethods = createFaveStarHandlers(
-    FAVESTAR_BASE_URL,
-    this,
-    (message: string) => {
-      this.props.addDangerToast(message);
-    },
-  );
-
-  columns = [
-    {
-      Cell: ({ row: { original } }: any) => {
-        return (
-          <FaveStar
-            itemId={original.id}
-            fetchFaveStar={this.fetchMethods.fetchFaveStar}
-            saveFaveStar={this.fetchMethods.saveFaveStar}
-            isStarred={!!this.state.favoriteStatus[original.id]}
-            height={20}
-          />
-        );
+  const columns = useMemo(
+    () => [
+      {
+        Cell: ({
+          row: {
+            original: { id },
+          },
+        }: any) => renderFaveStar(id),
+        Header: '',
+        id: 'favorite',
+        disableSortBy: true,
       },
-      Header: '',
-      id: 'favorite',
-      disableSortBy: true,
-    },
-    {
-      Cell: ({
-        row: {
-          original: { url, slice_name: sliceName },
-        },
-      }: any) => <a href={url}>{sliceName}</a>,
-      Header: t('Chart'),
-      accessor: 'slice_name',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { viz_type: vizType },
-        },
-      }: any) => vizType,
-      Header: t('Visualization Type'),
-      accessor: 'viz_type',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { datasource_name_text: dsNameTxt, datasource_url: dsUrl },
-        },
-      }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
-      Header: t('Datasource'),
-      accessor: 'datasource_name',
-    },
-    {
-      Cell: ({
-        row: {
-          original: {
-            changed_by_name: changedByName,
-            changed_by_url: changedByUrl,
+      {
+        Cell: ({
+          row: {
+            original: { url, slice_name: sliceName },
           },
-        },
-      }: any) => <a href={changedByUrl}>{changedByName}</a>,
-      Header: t('Modified By'),
-      accessor: 'changed_by.first_name',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { changed_on_delta_humanized: changedOn },
-        },
-      }: any) => <span className="no-wrap">{changedOn}</span>,
-      Header: t('Last Modified'),
-      accessor: 'changed_on_delta_humanized',
-    },
-    {
-      accessor: 'description',
-      hidden: true,
-      disableSortBy: true,
-    },
-    {
-      accessor: 'owners',
-      hidden: true,
-      disableSortBy: true,
-    },
-    {
-      accessor: 'datasource_id',
-      hidden: true,
-      disableSortBy: true,
-    },
-    {
-      Cell: ({ row: { original } }: any) => {
-        const handleDelete = () => this.handleChartDelete(original);
-        const openEditModal = () => this.openChartEditModal(original);
-        if (!this.canEdit && !this.canDelete) {
-          return null;
-        }
+        }: any) => <a href={url}>{sliceName}</a>,
+        Header: t('Chart'),
+        accessor: 'slice_name',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { viz_type: vizType },
+          },
+        }: any) => vizType,
+        Header: t('Visualization Type'),
+        accessor: 'viz_type',
+      },
+      {
+        Cell: ({
+          row: {
+            original: {
+              datasource_name_text: dsNameTxt,
+              datasource_url: dsUrl,
+            },
+          },
+        }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
+        Header: t('Datasource'),
+        accessor: 'datasource_name',
+      },
+      {
+        Cell: ({
+          row: {
+            original: {
+              changed_by_name: changedByName,
+              changed_by_url: changedByUrl,
+            },
+          },
+        }: any) => <a href={changedByUrl}>{changedByName}</a>,
+        Header: t('Modified By'),
+        accessor: 'changed_by.first_name',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_on_delta_humanized: changedOn },
+          },
+        }: any) => <span className="no-wrap">{changedOn}</span>,
+        Header: t('Last Modified'),
+        accessor: 'changed_on_delta_humanized',
+      },
+      {
+        accessor: 'description',
+        hidden: true,
+        disableSortBy: true,
+      },
+      {
+        accessor: 'owners',
+        hidden: true,
+        disableSortBy: true,
+      },
+      {
+        accessor: 'datasource_id',
+        hidden: true,
+        disableSortBy: true,
+      },
+      {
+        Cell: ({ row: { original } }: any) => {
+          const handleDelete = () => handleChartDelete(original);
+          const openEditModal = () => openChartEditModal(original);
+          if (!canEdit && !canDelete) {
+            return null;
+          }
 
-        return (
-          <span className="actions">
-            {this.canDelete && (
-              <ConfirmStatusChange
-                title={t('Please Confirm')}
-                description={
-                  <>
-                    {t('Are you sure you want to delete')}{' '}
-                    <b>{original.slice_name}</b>?
-                  </>
-                }
-                onConfirm={handleDelete}
-              >
-                {confirmDelete => (
-                  <span
-                    role="button"
-                    tabIndex={0}
-                    className="action-button"
-                    onClick={confirmDelete}
-                  >
-                    <Icon name="trash" />
-                  </span>
-                )}
-              </ConfirmStatusChange>
-            )}
-            {this.canEdit && (
-              <span
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                onClick={openEditModal}
-              >
-                <Icon name="pencil" />
-              </span>
-            )}
-          </span>
-        );
+          return (
+            <span className="actions">
+              {canDelete && (
+                <ConfirmStatusChange
+                  title={t('Please Confirm')}
+                  description={
+                    <>
+                      {t('Are you sure you want to delete')}{' '}
+                      <b>{original.slice_name}</b>?
+                    </>
+                  }
+                  onConfirm={handleDelete}
+                >
+                  {confirmDelete => (
+                    <span
+                      role="button"
+                      tabIndex={0}
+                      className="action-button"
+                      onClick={confirmDelete}
+                    >
+                      <Icon name="trash" />
+                    </span>
+                  )}
+                </ConfirmStatusChange>
+              )}
+              {canEdit && (
+                <span
+                  role="button"
+                  tabIndex={0}
+                  className="action-button"
+                  onClick={openEditModal}
+                >
+                  <Icon name="pencil" />
+                </span>
+              )}
+            </span>
+          );
+        },
+        Header: t('Actions'),
+        id: 'actions',
+        disableSortBy: true,
       },
-      Header: t('Actions'),
-      id: 'actions',
-      disableSortBy: true,
-    },
-  ];
+    ],
+    [canEdit, canDelete, favoriteStatusRef],
+  );
 
-  filters: Filters = [
+  const filters: Filters = [
     {
       Header: t('Owner'),
       id: 'owners',
@@ -296,7 +323,7 @@ class ChartList extends React.PureComponent<Props, State> {
         'chart',
         'owners',
         createErrorHandler(errMsg =>
-          this.props.addDangerToast(
+          props.addDangerToast(
             t(
               'An error occurred while fetching chart dataset values: %s',
               errMsg,
@@ -324,7 +351,7 @@ class ChartList extends React.PureComponent<Props, State> {
       unfilteredLabel: 'All',
       fetchSelects: createFetchDatasets(
         createErrorHandler(errMsg =>
-          this.props.addDangerToast(
+          props.addDangerToast(
             t(
               'An error occurred while fetching chart dataset values: %s',
               errMsg,
@@ -342,7 +369,7 @@ class ChartList extends React.PureComponent<Props, State> {
     },
   ];
 
-  sortTypes = [
+  const sortTypes = [
     {
       desc: false,
       id: 'slice_name',
@@ -363,139 +390,20 @@ class ChartList extends React.PureComponent<Props, 
State> {
     },
   ];
 
-  hasPerm = (perm: string) => {
-    if (!this.state.permissions.length) {
-      return false;
-    }
-
-    return this.state.permissions.some(p => p === perm);
-  };
-
-  toggleBulkSelect = () => {
-    this.setState({ bulkSelectEnabled: !this.state.bulkSelectEnabled });
-  };
-
-  openChartEditModal = (chart: Chart) => {
-    this.setState({
-      sliceCurrentlyEditing: {
-        slice_id: chart.id,
-        slice_name: chart.slice_name,
-        description: chart.description,
-        cache_timeout: chart.cache_timeout,
-      },
-    });
-  };
-
-  closeChartEditModal = () => {
-    this.setState({ sliceCurrentlyEditing: null });
-  };
-
-  handleChartUpdated = (edits: Chart) => {
-    // update the chart in our state with the edited info
-    const newCharts = this.state.charts.map(chart =>
-      chart.id === edits.id ? { ...chart, ...edits } : chart,
-    );
-    this.setState({
-      charts: newCharts,
-    });
-  };
-
-  handleChartDelete = ({ id, slice_name: sliceName }: Chart) => {
-    SupersetClient.delete({
-      endpoint: `/api/v1/chart/${id}`,
-    }).then(
-      () => {
-        const { lastFetchDataConfig } = this.state;
-        if (lastFetchDataConfig) {
-          this.fetchData(lastFetchDataConfig);
-        }
-        this.props.addSuccessToast(t('Deleted: %s', sliceName));
-      },
-      () => {
-        this.props.addDangerToast(
-          t('There was an issue deleting: %s', sliceName),
-        );
-      },
-    );
-  };
-
-  handleBulkChartDelete = (charts: Chart[]) => {
-    SupersetClient.delete({
-      endpoint: `/api/v1/chart/?q=${rison.encode(charts.map(({ id }) => id))}`,
-    }).then(
-      ({ json = {} }) => {
-        const { lastFetchDataConfig } = this.state;
-        if (lastFetchDataConfig) {
-          this.fetchData(lastFetchDataConfig);
-        }
-        this.props.addSuccessToast(json.message);
-      },
-      createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('There was an issue deleting the selected charts: %s', errMsg),
-        ),
-      ),
-    );
-  };
-
-  fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
-    // set loading state, cache the last config for fetching data in this 
component.
-    this.setState({
-      lastFetchDataConfig: {
-        filters,
-        pageIndex,
-        pageSize,
-        sortBy,
-      },
-      loading: true,
-    });
-
-    const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
-      col,
-      opr,
-      value,
-    }));
-
-    const queryParams = rison.encode({
-      order_column: sortBy[0].id,
-      order_direction: sortBy[0].desc ? 'desc' : 'asc',
-      page: pageIndex,
-      page_size: pageSize,
-      ...(filterExps.length ? { filters: filterExps } : {}),
-    });
-
-    return SupersetClient.get({
-      endpoint: `/api/v1/chart/?q=${queryParams}`,
-    })
-      .then(
-        ({ json = {} }) => {
-          this.setState({ charts: json.result, chartCount: json.count });
-        },
-        createErrorHandler(errMsg =>
-          this.props.addDangerToast(
-            t('An error occurred while fetching charts: %s', errMsg),
-          ),
-        ),
-      )
-      .finally(() => {
-        this.setState({ loading: false });
-      });
-  };
-
-  renderCard = (props: Chart & { loading: boolean }) => {
+  function renderCard(chart: Chart & { loading: boolean }) {
     const menu = (
       <Menu>
-        {this.canDelete && (
+        {canDelete && (
           <Menu.Item>
             <ConfirmStatusChange
               title={t('Please Confirm')}
               description={
                 <>
                   {t('Are you sure you want to delete')}{' '}
-                  <b>{props.slice_name}</b>?
+                  <b>{chart.slice_name}</b>?
                 </>
               }
-              onConfirm={() => this.handleChartDelete(props)}
+              onConfirm={() => handleChartDelete(chart)}
             >
               {confirmDelete => (
                 <div
@@ -510,11 +418,11 @@ class ChartList extends React.PureComponent<Props, State> 
{
             </ConfirmStatusChange>
           </Menu.Item>
         )}
-        {this.canEdit && (
+        {canEdit && (
           <Menu.Item
             role="button"
             tabIndex={0}
-            onClick={() => this.openChartEditModal(props)}
+            onClick={() => openChartEditModal(chart)}
           >
             <ListViewCard.MenuIcon name="pencil" /> Edit
           </Menu.Item>
@@ -524,16 +432,16 @@ class ChartList extends React.PureComponent<Props, State> 
{
 
     return (
       <ListViewCard
-        loading={props.loading}
-        title={props.slice_name}
-        url={this.state.bulkSelectEnabled ? undefined : props.url}
-        imgURL={props.thumbnail_url ?? ''}
+        loading={chart.loading}
+        title={chart.slice_name}
+        url={bulkSelectEnabled ? undefined : chart.url}
+        imgURL={chart.thumbnail_url ?? ''}
         imgFallbackURL={'/static/assets/images/chart-card-fallback.png'}
-        description={t('Last modified %s', props.changed_on_delta_humanized)}
-        coverLeft={(props.owners || []).slice(0, 5).map(owner => (
+        description={t('Last modified %s', chart.changed_on_delta_humanized)}
+        coverLeft={(chart.owners || []).slice(0, 5).map(owner => (
           <AvatarIcon
             key={owner.id}
-            uniqueKey={`${owner.username}-${props.id}`}
+            uniqueKey={`${owner.username}-${chart.id}`}
             firstName={owner.first_name}
             lastName={owner.last_name}
             iconSize={24}
@@ -541,18 +449,11 @@ class ChartList extends React.PureComponent<Props, State> 
{
           />
         ))}
         coverRight={
-          <Label bsStyle="secondary">{props.datasource_name_text}</Label>
+          <Label bsStyle="secondary">{chart.datasource_name_text}</Label>
         }
         actions={
           <ListViewCard.Actions>
-            <FaveStar
-              itemId={props.id}
-              fetchFaveStar={this.fetchMethods.fetchFaveStar}
-              saveFaveStar={this.fetchMethods.saveFaveStar}
-              isStarred={!!this.state.favoriteStatus[props.id]}
-              width={20}
-              height={20}
-            />
+            {renderFaveStar(chart.id)}
             <Dropdown overlay={menu}>
               <Icon name="more" />
             </Dropdown>
@@ -560,79 +461,68 @@ class ChartList extends React.PureComponent<Props, State> 
{
         }
       />
     );
-  };
+  }
 
-  render() {
-    const {
-      bulkSelectEnabled,
-      charts,
-      chartCount,
-      loading,
-      sliceCurrentlyEditing,
-    } = this.state;
-    return (
-      <>
-        <SubMenu
-          name={t('Charts')}
-          secondaryButton={
-            this.canDelete
-              ? {
-                  name: t('Bulk Select'),
-                  onClick: this.toggleBulkSelect,
-                }
-              : undefined
-          }
+  return (
+    <>
+      <SubMenu
+        name={t('Charts')}
+        secondaryButton={
+          canDelete
+            ? {
+                name: t('Bulk Select'),
+                onClick: toggleBulkSelect,
+              }
+            : undefined
+        }
+      />
+      {sliceCurrentlyEditing && (
+        <PropertiesModal
+          onHide={closeChartEditModal}
+          onSave={handleChartUpdated}
+          show
+          slice={sliceCurrentlyEditing}
         />
-        {sliceCurrentlyEditing && (
-          <PropertiesModal
-            onHide={this.closeChartEditModal}
-            onSave={this.handleChartUpdated}
-            show
-            slice={sliceCurrentlyEditing}
-          />
-        )}
-        <ConfirmStatusChange
-          title={t('Please confirm')}
-          description={t(
-            'Are you sure you want to delete the selected charts?',
-          )}
-          onConfirm={this.handleBulkChartDelete}
-        >
-          {confirmDelete => {
-            const bulkActions: ListViewProps['bulkActions'] = this.canDelete
-              ? [
-                  {
-                    key: 'delete',
-                    name: t('Delete'),
-                    onSelect: confirmDelete,
-                    type: 'danger',
-                  },
-                ]
-              : [];
-
-            return (
-              <ListView
-                bulkActions={bulkActions}
-                bulkSelectEnabled={bulkSelectEnabled}
-                cardSortSelectOptions={this.sortTypes}
-                className="chart-list-view"
-                columns={this.columns}
-                count={chartCount}
-                data={charts}
-                disableBulkSelect={this.toggleBulkSelect}
-                fetchData={this.fetchData}
-                filters={this.filters}
-                initialSort={this.initialSort}
-                loading={loading}
-                pageSize={PAGE_SIZE}
-                renderCard={this.renderCard}
-              />
-            );
-          }}
-        </ConfirmStatusChange>
-      </>
-    );
-  }
+      )}
+      <ConfirmStatusChange
+        title={t('Please confirm')}
+        description={t('Are you sure you want to delete the selected charts?')}
+        onConfirm={handleBulkChartDelete}
+      >
+        {confirmDelete => {
+          const bulkActions: ListViewProps['bulkActions'] = canDelete
+            ? [
+                {
+                  key: 'delete',
+                  name: t('Delete'),
+                  onSelect: confirmDelete,
+                  type: 'danger',
+                },
+              ]
+            : [];
+
+          return (
+            <ListView
+              bulkActions={bulkActions}
+              bulkSelectEnabled={bulkSelectEnabled}
+              cardSortSelectOptions={sortTypes}
+              className="chart-list-view"
+              columns={columns}
+              count={chartCount}
+              data={charts}
+              disableBulkSelect={toggleBulkSelect}
+              fetchData={fetchData}
+              filters={filters}
+              initialSort={initialSort}
+              loading={loading}
+              pageSize={PAGE_SIZE}
+              renderCard={renderCard}
+            />
+          );
+        }}
+      </ConfirmStatusChange>
+    </>
+  );
 }
 
 export default withToasts(ChartList);
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx 
b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index 437edbb..fded93a 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -18,22 +18,14 @@
  */
 import { SupersetClient } from '@superset-ui/connection';
 import { t } from '@superset-ui/translation';
-import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState, useMemo } from 'react';
 import rison from 'rison';
-import {
-  createFetchRelated,
-  createErrorHandler,
-  createFaveStarHandlers,
-} from 'src/views/CRUD/utils';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
 import AvatarIcon from 'src/components/AvatarIcon';
-import ListView, {
-  ListViewProps,
-  FetchDataConfig,
-  Filters,
-} from 'src/components/ListView';
+import ListView, { ListViewProps, Filters } from 'src/components/ListView';
 import ExpandableList from 'src/components/ExpandableList';
 import Owner from 'src/types/Owner';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -47,22 +39,11 @@ import { Dropdown, Menu } from 'src/common/components';
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
 
-interface Props {
+interface DashboardListProps {
   addDangerToast: (msg: string) => void;
   addSuccessToast: (msg: string) => void;
 }
 
-interface State {
-  bulkSelectEnabled: boolean;
-  dashboardCount: number;
-  dashboards: Dashboard[];
-  favoriteStatus: object;
-  dashboardToEdit: Dashboard | null;
-  lastFetchDataConfig: FetchDataConfig | null;
-  loading: boolean;
-  permissions: string[];
-}
-
 interface Dashboard {
   changed_by_name: string;
   changed_by_url: string;
@@ -76,157 +57,266 @@ interface Dashboard {
   owners: Owner[];
 }
 
-class DashboardList extends React.PureComponent<Props, State> {
-  static propTypes = {
-    addDangerToast: PropTypes.func.isRequired,
-  };
+function DashboardList(props: DashboardListProps) {
+  const {
+    state: {
+      loading,
+      resourceCount: dashboardCount,
+      resourceCollection: dashboards,
+      bulkSelectEnabled,
+    },
+    setResourceCollection: setDashboards,
+    hasPerm,
+    fetchData,
+    toggleBulkSelect,
+    refreshData,
+  } = useListViewResource<Dashboard>(
+    'dashboard',
+    t('dashboard'),
+    props.addDangerToast,
+  );
+  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
+    {},
+    FAVESTAR_BASE_URL,
+    props.addDangerToast,
+  );
+
+  const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
+    null,
+  );
 
-  state: State = {
-    bulkSelectEnabled: false,
-    dashboardCount: 0,
-    dashboards: [],
-    favoriteStatus: {}, // Hash mapping dashboard id to 'isStarred' status
-    dashboardToEdit: null,
-    lastFetchDataConfig: null,
-    loading: true,
-    permissions: [],
-  };
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+  const canExport = hasPerm('can_mulexport');
+
+  const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+
+  function openDashboardEditModal(dashboard: Dashboard) {
+    setDashboardToEdit(dashboard);
+  }
 
-  componentDidMount() {
-    SupersetClient.get({
-      endpoint: `/api/v1/dashboard/_info`,
+  function handleDashboardEdit(edits: Dashboard) {
+    return SupersetClient.get({
+      endpoint: `/api/v1/dashboard/${edits.id}`,
     }).then(
-      ({ json: infoJson = {} }) => {
-        this.setState({
-          permissions: infoJson.permissions,
-        });
+      ({ json = {} }) => {
+        setDashboards(
+          dashboards.map(dashboard => {
+            if (dashboard.id === json.id) {
+              return json.result;
+            }
+            return dashboard;
+          }),
+        );
       },
       createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('An error occurred while fetching Dashboards: %s, %s', errMsg),
+        props.addDangerToast(
+          t('An error occurred while fetching dashboards: %s', errMsg),
         ),
       ),
     );
   }
 
-  get canEdit() {
-    return this.hasPerm('can_edit');
+  function handleDashboardDelete({
+    id,
+    dashboard_title: dashboardTitle,
+  }: Dashboard) {
+    return SupersetClient.delete({
+      endpoint: `/api/v1/dashboard/${id}`,
+    }).then(
+      () => {
+        refreshData();
+        props.addSuccessToast(t('Deleted: %s', dashboardTitle));
+      },
+      createErrorHandler(errMsg =>
+        props.addDangerToast(
+          t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
+        ),
+      ),
+    );
   }
 
-  get canDelete() {
-    return this.hasPerm('can_delete');
+  function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) {
+    return SupersetClient.delete({
+      endpoint: `/api/v1/dashboard/?q=${rison.encode(
+        dashboardsToDelete.map(({ id }) => id),
+      )}`,
+    }).then(
+      ({ json = {} }) => {
+        props.addSuccessToast(json.message);
+      },
+      createErrorHandler(errMsg =>
+        props.addDangerToast(
+          t('There was an issue deleting the selected dashboards: ', errMsg),
+        ),
+      ),
+    );
   }
 
-  get canExport() {
-    return this.hasPerm('can_mulexport');
+  function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
+    return window.location.assign(
+      `/api/v1/dashboard/export/?q=${rison.encode(
+        dashboardsToExport.map(({ id }) => id),
+      )}`,
+    );
   }
 
-  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-
-  fetchMethods = createFaveStarHandlers(
-    FAVESTAR_BASE_URL,
-    this,
-    (message: string) => {
-      this.props.addDangerToast(message);
-    },
-  );
+  function renderFaveStar(id: number) {
+    return (
+      <FaveStar
+        itemId={id}
+        fetchFaveStar={fetchFaveStar}
+        saveFaveStar={saveFaveStar}
+        isStarred={!!favoriteStatusRef.current[id]}
+        height={20}
+        width={20}
+      />
+    );
+  }
 
-  columns = [
-    {
-      Cell: ({ row: { original } }: any) => {
-        return (
-          <FaveStar
-            itemId={original.id}
-            fetchFaveStar={this.fetchMethods.fetchFaveStar}
-            saveFaveStar={this.fetchMethods.saveFaveStar}
-            isStarred={!!this.state.favoriteStatus[original.id]}
-            height={20}
+  const columns = useMemo(
+    () => [
+      {
+        Cell: ({
+          row: {
+            original: { id },
+          },
+        }: any) => renderFaveStar(id),
+        Header: '',
+        id: 'favorite',
+        disableSortBy: true,
+      },
+      {
+        Cell: ({
+          row: {
+            original: { url, dashboard_title: dashboardTitle },
+          },
+        }: any) => <a href={url}>{dashboardTitle}</a>,
+        Header: t('Title'),
+        accessor: 'dashboard_title',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { owners },
+          },
+        }: any) => (
+          <ExpandableList
+            items={owners.map(
+              ({ first_name: firstName, last_name: lastName }: any) =>
+                `${firstName} ${lastName}`,
+            )}
+            display={2}
           />
-        );
+        ),
+        Header: t('Owners'),
+        accessor: 'owners',
+        disableSortBy: true,
       },
-      Header: '',
-      id: 'favorite',
-      disableSortBy: true,
-    },
-    {
-      Cell: ({
-        row: {
-          original: { url, dashboard_title: dashboardTitle },
-        },
-      }: any) => <a href={url}>{dashboardTitle}</a>,
-      Header: t('Title'),
-      accessor: 'dashboard_title',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { owners },
-        },
-      }: any) => (
-        <ExpandableList
-          items={owners.map(
-            ({ first_name: firstName, last_name: lastName }: any) =>
-              `${firstName} ${lastName}`,
-          )}
-          display={2}
-        />
-      ),
-      Header: t('Owners'),
-      accessor: 'owners',
-      disableSortBy: true,
-    },
-    {
-      Cell: ({
-        row: {
-          original: {
-            changed_by_name: changedByName,
-            changed_by_url: changedByUrl,
+      {
+        Cell: ({
+          row: {
+            original: {
+              changed_by_name: changedByName,
+              changed_by_url: changedByUrl,
+            },
           },
+        }: any) => <a href={changedByUrl}>{changedByName}</a>,
+        Header: t('Modified By'),
+        accessor: 'changed_by.first_name',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { published },
+          },
+        }: any) => (
+          <span className="no-wrap">
+            {published ? <Icon name="check" /> : ''}
+          </span>
+        ),
+        Header: t('Published'),
+        accessor: 'published',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_on_delta_humanized: changedOn },
+          },
+        }: any) => <span className="no-wrap">{changedOn}</span>,
+        Header: t('Modified'),
+        accessor: 'changed_on_delta_humanized',
+      },
+      {
+        accessor: 'slug',
+        hidden: true,
+        disableSortBy: true,
+      },
+      {
+        Cell: ({ row: { original } }: any) => {
+          const handleDelete = () => handleDashboardDelete(original);
+          const handleEdit = () => openDashboardEditModal(original);
+          const handleExport = () => handleBulkDashboardExport([original]);
+          if (!canEdit && !canDelete && !canExport) {
+            return null;
+          }
+          return (
+            <span className="actions">
+              {canDelete && (
+                <ConfirmStatusChange
+                  title={t('Please Confirm')}
+                  description={
+                    <>
+                      {t('Are you sure you want to delete')}{' '}
+                      <b>{original.dashboard_title}</b>?
+                    </>
+                  }
+                  onConfirm={handleDelete}
+                >
+                  {confirmDelete => (
+                    <span
+                      role="button"
+                      tabIndex={0}
+                      className="action-button"
+                      onClick={confirmDelete}
+                    >
+                      <Icon name="trash" />
+                    </span>
+                  )}
+                </ConfirmStatusChange>
+              )}
+              {canExport && (
+                <span
+                  role="button"
+                  tabIndex={0}
+                  className="action-button"
+                  onClick={handleExport}
+                >
+                  <Icon name="share" />
+                </span>
+              )}
+              {canEdit && (
+                <span
+                  role="button"
+                  tabIndex={0}
+                  className="action-button"
+                  onClick={handleEdit}
+                >
+                  <Icon name="pencil" />
+                </span>
+              )}
+            </span>
+          );
         },
-      }: any) => <a href={changedByUrl}>{changedByName}</a>,
-      Header: t('Modified By'),
-      accessor: 'changed_by.first_name',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { published },
-        },
-      }: any) => (
-        <span className="no-wrap">
-          {published ? <Icon name="check" /> : ''}
-        </span>
-      ),
-      Header: t('Published'),
-      accessor: 'published',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { changed_on_delta_humanized: changedOn },
-        },
-      }: any) => <span className="no-wrap">{changedOn}</span>,
-      Header: t('Modified'),
-      accessor: 'changed_on_delta_humanized',
-    },
-    {
-      accessor: 'slug',
-      hidden: true,
-      disableSortBy: true,
-    },
-    {
-      Cell: ({ row: { original } }: any) => this.renderActions(original),
-      Header: t('Actions'),
-      id: 'actions',
-      disableSortBy: true,
-    },
-  ];
-
-  toggleBulkSelect = () => {
-    this.setState({ bulkSelectEnabled: !this.state.bulkSelectEnabled });
-  };
+        Header: t('Actions'),
+        id: 'actions',
+        disableSortBy: true,
+      },
+    ],
+    [canEdit, canDelete, canExport, favoriteStatusRef],
+  );
 
-  filters: Filters = [
+  const filters: Filters = [
     {
       Header: 'Owner',
       id: 'owners',
@@ -237,7 +327,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         'dashboard',
         'owners',
         createErrorHandler(errMsg =>
-          this.props.addDangerToast(
+          props.addDangerToast(
             t(
               'An error occurred while fetching chart owner values: %s',
               errMsg,
@@ -266,7 +356,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     },
   ];
 
-  sortTypes = [
+  const sortTypes = [
     {
       desc: false,
       id: 'dashboard_title',
@@ -287,210 +377,20 @@ class DashboardList extends React.PureComponent<Props, 
State> {
     },
   ];
 
-  hasPerm = (perm: string) => {
-    if (!this.state.permissions.length) {
-      return false;
-    }
-
-    return Boolean(this.state.permissions.find(p => p === perm));
-  };
-
-  openDashboardEditModal = (dashboard: Dashboard) => {
-    this.setState({
-      dashboardToEdit: dashboard,
-    });
-  };
-
-  handleDashboardEdit = (edits: any) => {
-    this.setState({ loading: true });
-    return SupersetClient.get({
-      endpoint: `/api/v1/dashboard/${edits.id}`,
-    }).then(
-      ({ json = {} }) => {
-        this.setState({
-          dashboards: this.state.dashboards.map(dashboard => {
-            if (dashboard.id === json.id) {
-              return json.result;
-            }
-            return dashboard;
-          }),
-          loading: false,
-        });
-      },
-      createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('An error occurred while fetching dashboards: %s', errMsg),
-        ),
-      ),
-    );
-  };
-
-  handleDashboardDelete = ({
-    id,
-    dashboard_title: dashboardTitle,
-  }: Dashboard) =>
-    SupersetClient.delete({
-      endpoint: `/api/v1/dashboard/${id}`,
-    }).then(
-      () => {
-        const { lastFetchDataConfig } = this.state;
-        if (lastFetchDataConfig) {
-          this.fetchData(lastFetchDataConfig);
-        }
-        this.props.addSuccessToast(t('Deleted: %s', dashboardTitle));
-      },
-      createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
-        ),
-      ),
-    );
-
-  handleBulkDashboardDelete = (dashboards: Dashboard[]) => {
-    SupersetClient.delete({
-      endpoint: `/api/v1/dashboard/?q=${rison.encode(
-        dashboards.map(({ id }) => id),
-      )}`,
-    }).then(
-      ({ json = {} }) => {
-        const { lastFetchDataConfig } = this.state;
-        if (lastFetchDataConfig) {
-          this.fetchData(lastFetchDataConfig);
-        }
-        this.props.addSuccessToast(json.message);
-      },
-      createErrorHandler(errMsg =>
-        this.props.addDangerToast(
-          t('There was an issue deleting the selected dashboards: ', errMsg),
-        ),
-      ),
-    );
-  };
-
-  handleBulkDashboardExport = (dashboards: Dashboard[]) => {
-    return window.location.assign(
-      `/api/v1/dashboard/export/?q=${rison.encode(
-        dashboards.map(({ id }) => id),
-      )}`,
-    );
-  };
-
-  fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
-    // set loading state, cache the last config for fetching data in this 
component.
-    this.setState({
-      lastFetchDataConfig: {
-        filters,
-        pageIndex,
-        pageSize,
-        sortBy,
-      },
-      loading: true,
-    });
-    const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
-      col,
-      opr,
-      value,
-    }));
-
-    const queryParams = rison.encode({
-      order_column: sortBy[0].id,
-      order_direction: sortBy[0].desc ? 'desc' : 'asc',
-      page: pageIndex,
-      page_size: pageSize,
-      ...(filterExps.length ? { filters: filterExps } : {}),
-    });
-
-    return SupersetClient.get({
-      endpoint: `/api/v1/dashboard/?q=${queryParams}`,
-    })
-      .then(
-        ({ json = {} }) => {
-          this.setState({
-            dashboards: json.result,
-            dashboardCount: json.count,
-          });
-        },
-        createErrorHandler(errMsg =>
-          this.props.addDangerToast(
-            t('An error occurred while fetching dashboards: %s', errMsg),
-          ),
-        ),
-      )
-      .finally(() => {
-        this.setState({ loading: false });
-      });
-  };
-
-  renderActions(original: Dashboard) {
-    const handleDelete = () => this.handleDashboardDelete(original);
-    const handleEdit = () => this.openDashboardEditModal(original);
-    const handleExport = () => this.handleBulkDashboardExport([original]);
-    if (!this.canEdit && !this.canDelete && !this.canExport) {
-      return null;
-    }
-    return (
-      <span className="actions">
-        {this.canDelete && (
-          <ConfirmStatusChange
-            title={t('Please Confirm')}
-            description={
-              <>
-                {t('Are you sure you want to delete')}{' '}
-                <b>{original.dashboard_title}</b>?
-              </>
-            }
-            onConfirm={handleDelete}
-          >
-            {confirmDelete => (
-              <span
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                onClick={confirmDelete}
-              >
-                <Icon name="trash" />
-              </span>
-            )}
-          </ConfirmStatusChange>
-        )}
-        {this.canExport && (
-          <span
-            role="button"
-            tabIndex={0}
-            className="action-button"
-            onClick={handleExport}
-          >
-            <Icon name="share" />
-          </span>
-        )}
-        {this.canEdit && (
-          <span
-            role="button"
-            tabIndex={0}
-            className="action-button"
-            onClick={handleEdit}
-          >
-            <Icon name="pencil" />
-          </span>
-        )}
-      </span>
-    );
-  }
-
-  renderCard = (props: Dashboard & { loading: boolean }) => {
+  function renderCard(dashboard: Dashboard & { loading: boolean }) {
     const menu = (
       <Menu>
-        {this.canDelete && (
+        {canDelete && (
           <Menu.Item>
             <ConfirmStatusChange
               title={t('Please Confirm')}
               description={
                 <>
                   {t('Are you sure you want to delete')}{' '}
-                  <b>{props.dashboard_title}</b>?
+                  <b>{dashboard.dashboard_title}</b>?
                 </>
               }
-              onConfirm={() => this.handleDashboardDelete(props)}
+              onConfirm={() => handleDashboardDelete(dashboard)}
             >
               {confirmDelete => (
                 <div
@@ -505,20 +405,20 @@ class DashboardList extends React.PureComponent<Props, 
State> {
             </ConfirmStatusChange>
           </Menu.Item>
         )}
-        {this.canExport && (
+        {canExport && (
           <Menu.Item
             role="button"
             tabIndex={0}
-            onClick={() => this.handleBulkDashboardExport([props])}
+            onClick={() => handleBulkDashboardExport([dashboard])}
           >
             <ListViewCard.MenuIcon name="share" /> Export
           </Menu.Item>
         )}
-        {this.canEdit && (
+        {canEdit && (
           <Menu.Item
             role="button"
             tabIndex={0}
-            onClick={() => this.openDashboardEditModal(props)}
+            onClick={() => openDashboardEditModal(dashboard)}
           >
             <ListViewCard.MenuIcon name="pencil" /> Edit
           </Menu.Item>
@@ -528,17 +428,22 @@ class DashboardList extends React.PureComponent<Props, 
State> {
 
     return (
       <ListViewCard
-        title={props.dashboard_title}
-        loading={props.loading}
-        titleRight={<Label>{props.published ? 'published' : 'draft'}</Label>}
-        url={this.state.bulkSelectEnabled ? undefined : props.url}
-        imgURL={props.thumbnail_url}
+        loading={dashboard.loading}
+        title={dashboard.dashboard_title}
+        titleRight={
+          <Label>{dashboard.published ? 'published' : 'draft'}</Label>
+        }
+        url={bulkSelectEnabled ? undefined : dashboard.url}
+        imgURL={dashboard.thumbnail_url}
         imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
-        description={t('Last modified %s', props.changed_on_delta_humanized)}
-        coverLeft={(props.owners || []).slice(0, 5).map(owner => (
+        description={t(
+          'Last modified %s',
+          dashboard.changed_on_delta_humanized,
+        )}
+        coverLeft={(dashboard.owners || []).slice(0, 5).map(owner => (
           <AvatarIcon
             key={owner.id}
-            uniqueKey={`${owner.username}-${props.id}`}
+            uniqueKey={`${owner.username}-${dashboard.id}`}
             firstName={owner.first_name}
             lastName={owner.last_name}
             iconSize={24}
@@ -547,14 +452,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         ))}
         actions={
           <ListViewCard.Actions>
-            <FaveStar
-              itemId={props.id}
-              fetchFaveStar={this.fetchMethods.fetchFaveStar}
-              saveFaveStar={this.fetchMethods.saveFaveStar}
-              isStarred={!!this.state.favoriteStatus[props.id]}
-              width={20}
-              height={20}
-            />
+            {renderFaveStar(dashboard.id)}
             <Dropdown overlay={menu}>
               <Icon name="more" />
             </Dropdown>
@@ -562,87 +460,78 @@ class DashboardList extends React.PureComponent<Props, 
State> {
         }
       />
     );
-  };
+  }
 
-  render() {
-    const {
-      bulkSelectEnabled,
-      dashboards,
-      dashboardCount,
-      loading,
-      dashboardToEdit,
-    } = this.state;
-    return (
-      <>
-        <SubMenu
-          name={t('Dashboards')}
-          secondaryButton={
-            this.canDelete || this.canExport
-              ? {
-                  name: t('Bulk Select'),
-                  onClick: this.toggleBulkSelect,
-                }
-              : undefined
+  return (
+    <>
+      <SubMenu
+        name={t('Dashboards')}
+        secondaryButton={
+          canDelete || canExport
+            ? {
+                name: t('Bulk Select'),
+                onClick: toggleBulkSelect,
+              }
+            : undefined
+        }
+      />
+      <ConfirmStatusChange
+        title={t('Please confirm')}
+        description={t(
+          'Are you sure you want to delete the selected dashboards?',
+        )}
+        onConfirm={handleBulkDashboardDelete}
+      >
+        {confirmDelete => {
+          const bulkActions: ListViewProps['bulkActions'] = [];
+          if (canDelete) {
+            bulkActions.push({
+              key: 'delete',
+              name: t('Delete'),
+              type: 'danger',
+              onSelect: confirmDelete,
+            });
           }
-        />
-        <ConfirmStatusChange
-          title={t('Please confirm')}
-          description={t(
-            'Are you sure you want to delete the selected dashboards?',
-          )}
-          onConfirm={this.handleBulkDashboardDelete}
-        >
-          {confirmDelete => {
-            const bulkActions: ListViewProps['bulkActions'] = [];
-            if (this.canDelete) {
-              bulkActions.push({
-                key: 'delete',
-                name: t('Delete'),
-                type: 'danger',
-                onSelect: confirmDelete,
-              });
-            }
-            if (this.canExport) {
-              bulkActions.push({
-                key: 'export',
-                name: t('Export'),
-                type: 'primary',
-                onSelect: this.handleBulkDashboardExport,
-              });
-            }
-            return (
-              <>
-                {dashboardToEdit && (
-                  <PropertiesModal
-                    dashboardId={dashboardToEdit.id}
-                    show
-                    onHide={() => this.setState({ dashboardToEdit: null })}
-                    onSubmit={this.handleDashboardEdit}
-                  />
-                )}
-                <ListView
-                  bulkActions={bulkActions}
-                  bulkSelectEnabled={bulkSelectEnabled}
-                  cardSortSelectOptions={this.sortTypes}
-                  className="dashboard-list-view"
-                  columns={this.columns}
-                  count={dashboardCount}
-                  data={dashboards}
-                  disableBulkSelect={this.toggleBulkSelect}
-                  fetchData={this.fetchData}
-                  filters={this.filters}
-                  initialSort={this.initialSort}
-                  loading={loading}
-                  pageSize={PAGE_SIZE}
-                  renderCard={this.renderCard}
+          if (canExport) {
+            bulkActions.push({
+              key: 'export',
+              name: t('Export'),
+              type: 'primary',
+              onSelect: handleBulkDashboardExport,
+            });
+          }
+          return (
+            <>
+              {dashboardToEdit && (
+                <PropertiesModal
+                  dashboardId={dashboardToEdit.id}
+                  show
+                  onHide={() => setDashboardToEdit(null)}
+                  onSubmit={handleDashboardEdit}
                 />
-              </>
-            );
-          }}
-        </ConfirmStatusChange>
-      </>
-    );
-  }
+              )}
+              <ListView
+                bulkActions={bulkActions}
+                bulkSelectEnabled={bulkSelectEnabled}
+                cardSortSelectOptions={sortTypes}
+                className="dashboard-list-view"
+                columns={columns}
+                count={dashboardCount}
+                data={dashboards}
+                disableBulkSelect={toggleBulkSelect}
+                fetchData={fetchData}
+                filters={filters}
+                initialSort={initialSort}
+                loading={loading}
+                pageSize={PAGE_SIZE}
+                renderCard={renderCard}
+              />
+            </>
+          );
+        }}
+      </ConfirmStatusChange>
+    </>
+  );
 }
 
 export default withToasts(DashboardList);
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx 
b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index f7a5ab0..668739c 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -18,22 +18,14 @@
  */
 import { SupersetClient } from '@superset-ui/connection';
 import { t } from '@superset-ui/translation';
-import React, {
-  FunctionComponent,
-  useCallback,
-  useEffect,
-  useState,
-} from 'react';
+import React, { FunctionComponent, useState, useMemo } from 'react';
 import rison from 'rison';
 import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource } from 'src/views/CRUD/hooks';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import DatasourceModal from 'src/datasource/DatasourceModal';
 import DeleteModal from 'src/components/DeleteModal';
-import ListView, {
-  ListViewProps,
-  FetchDataConfig,
-  Filters,
-} from 'src/components/ListView';
+import ListView, { ListViewProps, Filters } from 'src/components/ListView';
 import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
 import { commonMenuData } from 'src/views/CRUD/data/common';
 import AvatarIcon from 'src/components/AvatarIcon';
@@ -54,6 +46,7 @@ type Dataset = {
     id: string;
     database_name: string;
   };
+  kind: string;
   explore_url: string;
   id: number;
   owners: Array<Owner>;
@@ -104,118 +97,31 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
   addDangerToast,
   addSuccessToast,
 }) => {
-  const [datasetCount, setDatasetCount] = useState(0);
+  const {
+    state: {
+      loading,
+      resourceCount: datasetCount,
+      resourceCollection: datasets,
+      bulkSelectEnabled,
+    },
+    hasPerm,
+    fetchData,
+    toggleBulkSelect,
+    refreshData,
+  } = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
+
+  const [datasetAddModalOpen, setDatasetAddModalOpen] = useState<boolean>(
+    false,
+  );
+
   const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
     (Dataset & { chart_count: number; dashboard_count: number }) | null
   >(null);
+
   const [
     datasetCurrentlyEditing,
     setDatasetCurrentlyEditing,
   ] = useState<Dataset | null>(null);
-  const [datasets, setDatasets] = useState<any[]>([]);
-  const [
-    lastFetchDataConfig,
-    setLastFetchDataConfig,
-  ] = useState<FetchDataConfig | null>(null);
-  const [loading, setLoading] = useState(true);
-  const [permissions, setPermissions] = useState<string[]>([]);
-
-  const [datasetAddModalOpen, setDatasetAddModalOpen] = useState<boolean>(
-    false,
-  );
-  const [bulkSelectEnabled, setBulkSelectEnabled] = useState<boolean>(false);
-
-  const filterTypes: Filters = [
-    {
-      Header: t('Owner'),
-      id: 'owners',
-      input: 'select',
-      operator: 'rel_m_m',
-      unfilteredLabel: 'All',
-      fetchSelects: createFetchRelated(
-        'dataset',
-        'owners',
-        createErrorHandler(errMsg =>
-          t(
-            'An error occurred while fetching dataset owner values: %s',
-            errMsg,
-          ),
-        ),
-      ),
-      paginate: true,
-    },
-    {
-      Header: t('Datasource'),
-      id: 'database',
-      input: 'select',
-      operator: 'rel_o_m',
-      unfilteredLabel: 'All',
-      fetchSelects: createFetchRelated(
-        'dataset',
-        'database',
-        createErrorHandler(errMsg =>
-          t(
-            'An error occurred while fetching dataset datasource values: %s',
-            errMsg,
-          ),
-        ),
-      ),
-      paginate: true,
-    },
-    {
-      Header: t('Schema'),
-      id: 'schema',
-      input: 'select',
-      operator: 'eq',
-      unfilteredLabel: 'All',
-      fetchSelects: createFetchSchemas(errMsg =>
-        t('An error occurred while fetching schema values: %s', errMsg),
-      ),
-      paginate: true,
-    },
-    {
-      Header: t('Type'),
-      id: 'is_sqllab_view',
-      input: 'select',
-      operator: 'eq',
-      unfilteredLabel: 'All',
-      selects: [
-        { label: 'Virtual', value: true },
-        { label: 'Physical', value: false },
-      ],
-    },
-    {
-      Header: t('Search'),
-      id: 'table_name',
-      input: 'search',
-      operator: 'ct',
-    },
-  ];
-
-  const fetchDatasetInfo = () => {
-    SupersetClient.get({
-      endpoint: `/api/v1/dataset/_info`,
-    }).then(
-      ({ json: infoJson = {} }) => {
-        setPermissions(infoJson.permissions);
-      },
-      createErrorHandler(errMsg =>
-        addDangerToast(t('An error occurred while fetching datasets', errMsg)),
-      ),
-    );
-  };
-
-  useEffect(() => {
-    fetchDatasetInfo();
-  }, []);
-
-  const hasPerm = (perm: string) => {
-    if (!permissions.length) {
-      return false;
-    }
-
-    return Boolean(permissions.find(p => p === perm));
-  };
 
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
@@ -258,187 +164,260 @@ const DatasetList: FunctionComponent<DatasetListProps> 
= ({
         ),
       );
 
-  const columns = [
-    {
-      Cell: ({
-        row: {
-          original: { kind },
-        },
-      }: any) => {
-        if (kind === 'physical')
+  const columns = useMemo(
+    () => [
+      {
+        Cell: ({
+          row: {
+            original: { kind },
+          },
+        }: any) => {
+          if (kind === 'physical')
+            return (
+              <TooltipWrapper
+                label="physical-dataset"
+                tooltip={t('Physical Dataset')}
+              >
+                <Icon name="dataset-physical" />
+              </TooltipWrapper>
+            );
+
           return (
             <TooltipWrapper
-              label="physical-dataset"
-              tooltip={t('Physical Dataset')}
+              label="virtual-dataset"
+              tooltip={t('Virtual Dataset')}
             >
-              <Icon name="dataset-physical" />
+              <Icon name="dataset-virtual" />
             </TooltipWrapper>
           );
-
-        return (
-          <TooltipWrapper
-            label="virtual-dataset"
-            tooltip={t('Virtual Dataset')}
-          >
-            <Icon name="dataset-virtual" />
-          </TooltipWrapper>
-        );
-      },
-      accessor: 'kind_icon',
-      disableSortBy: true,
-      size: 'xs',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { table_name: datasetTitle },
-        },
-      }: any) => datasetTitle,
-      Header: t('Name'),
-      accessor: 'table_name',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { kind },
         },
-      }: any) => kind[0]?.toUpperCase() + kind.slice(1),
-      Header: t('Type'),
-      accessor: 'kind',
-      disableSortBy: true,
-      size: 'md',
-    },
-    {
-      Header: t('Source'),
-      accessor: 'database.database_name',
-      size: 'lg',
-    },
-    {
-      Header: t('Schema'),
-      accessor: 'schema',
-      size: 'lg',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { changed_on_delta_humanized: changedOn },
-        },
-      }: any) => <span className="no-wrap">{changedOn}</span>,
-      Header: t('Modified'),
-      accessor: 'changed_on_delta_humanized',
-      size: 'xl',
-    },
-    {
-      Cell: ({
-        row: {
-          original: { changed_by_name: changedByName },
-        },
-      }: any) => changedByName,
-      Header: t('Modified By'),
-      accessor: 'changed_by.first_name',
-      size: 'xl',
-    },
-    {
-      accessor: 'database',
-      disableSortBy: true,
-      hidden: true,
-    },
-    {
-      Cell: ({
-        row: {
-          original: { owners, table_name: tableName },
+        accessor: 'kind_icon',
+        disableSortBy: true,
+        size: 'xs',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { table_name: datasetTitle },
+          },
+        }: any) => datasetTitle,
+        Header: t('Name'),
+        accessor: 'table_name',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { kind },
+          },
+        }: any) => kind[0]?.toUpperCase() + kind.slice(1),
+        Header: t('Type'),
+        accessor: 'kind',
+        disableSortBy: true,
+        size: 'md',
+      },
+      {
+        Header: t('Source'),
+        accessor: 'database.database_name',
+        size: 'lg',
+      },
+      {
+        Header: t('Schema'),
+        accessor: 'schema',
+        size: 'lg',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_on_delta_humanized: changedOn },
+          },
+        }: any) => <span className="no-wrap">{changedOn}</span>,
+        Header: t('Modified'),
+        accessor: 'changed_on_delta_humanized',
+        size: 'xl',
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_by_name: changedByName },
+          },
+        }: any) => changedByName,
+        Header: t('Modified By'),
+        accessor: 'changed_by.first_name',
+        size: 'xl',
+      },
+      {
+        accessor: 'database',
+        disableSortBy: true,
+        hidden: true,
+      },
+      {
+        Cell: ({
+          row: {
+            original: { owners, table_name: tableName },
+          },
+        }: any) => {
+          if (!owners) {
+            return null;
+          }
+          return owners
+            .slice(0, 5)
+            .map((owner: Owner) => (
+              <AvatarIcon
+                key={owner.id}
+                uniqueKey={`${tableName}-${owner.username}`}
+                firstName={owner.first_name}
+                lastName={owner.last_name}
+                iconSize={24}
+                textSize={9}
+              />
+            ));
         },
-      }: any) => {
-        if (!owners) {
-          return null;
-        }
-        return owners
-          .slice(0, 5)
-          .map((owner: Owner) => (
-            <AvatarIcon
-              key={owner.id}
-              uniqueKey={`${tableName}-${owner.username}`}
-              firstName={owner.first_name}
-              lastName={owner.last_name}
-              iconSize={24}
-              textSize={9}
-            />
-          ));
+        Header: t('Owners'),
+        id: 'owners',
+        disableSortBy: true,
+        size: 'lg',
       },
-      Header: t('Owners'),
-      id: 'owners',
-      disableSortBy: true,
-      size: 'lg',
-    },
-    {
-      accessor: 'is_sqllab_view',
-      hidden: true,
-      disableSortBy: true,
-    },
-    {
-      Cell: ({ row: { original } }: any) => {
-        const handleEdit = () => openDatasetEditModal(original);
-        const handleDelete = () => openDatasetDeleteModal(original);
-        if (!canEdit && !canDelete) {
-          return null;
-        }
-        return (
-          <span className="actions">
-            <TooltipWrapper
-              label="explore-action"
-              tooltip={t('Explore')}
-              placement="bottom"
-            >
-              <a
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                href={original.explore_url}
-              >
-                <Icon name="compass" />
-              </a>
-            </TooltipWrapper>
-            {canDelete && (
+      {
+        accessor: 'is_sqllab_view',
+        hidden: true,
+        disableSortBy: true,
+      },
+      {
+        Cell: ({ row: { original } }: any) => {
+          const handleEdit = () => openDatasetEditModal(original);
+          const handleDelete = () => openDatasetDeleteModal(original);
+          if (!canEdit && !canDelete) {
+            return null;
+          }
+          return (
+            <span className="actions">
               <TooltipWrapper
-                label="delete-action"
-                tooltip={t('Delete')}
+                label="explore-action"
+                tooltip={t('Explore')}
                 placement="bottom"
               >
-                <span
+                <a
                   role="button"
                   tabIndex={0}
                   className="action-button"
-                  onClick={handleDelete}
+                  href={original.explore_url}
                 >
-                  <Icon name="trash" />
-                </span>
+                  <Icon name="compass" />
+                </a>
               </TooltipWrapper>
-            )}
+              {canDelete && (
+                <TooltipWrapper
+                  label="delete-action"
+                  tooltip={t('Delete')}
+                  placement="bottom"
+                >
+                  <span
+                    role="button"
+                    tabIndex={0}
+                    className="action-button"
+                    onClick={handleDelete}
+                  >
+                    <Icon name="trash" />
+                  </span>
+                </TooltipWrapper>
+              )}
 
-            {canEdit && (
-              <TooltipWrapper
-                label="edit-action"
-                tooltip={t('Edit')}
-                placement="bottom"
-              >
-                <span
-                  role="button"
-                  tabIndex={0}
-                  className="action-button"
-                  onClick={handleEdit}
+              {canEdit && (
+                <TooltipWrapper
+                  label="edit-action"
+                  tooltip={t('Edit')}
+                  placement="bottom"
                 >
-                  <Icon name="pencil" />
-                </span>
-              </TooltipWrapper>
-            )}
-          </span>
-        );
+                  <span
+                    role="button"
+                    tabIndex={0}
+                    className="action-button"
+                    onClick={handleEdit}
+                  >
+                    <Icon name="pencil" />
+                  </span>
+                </TooltipWrapper>
+              )}
+            </span>
+          );
+        },
+        Header: t('Actions'),
+        id: 'actions',
+        disableSortBy: true,
       },
-      Header: t('Actions'),
-      id: 'actions',
-      disableSortBy: true,
-    },
-  ];
+    ],
+    [canCreate, canEdit, canDelete],
+  );
+
+  const filterTypes: Filters = useMemo(
+    () => [
+      {
+        Header: t('Owner'),
+        id: 'owners',
+        input: 'select',
+        operator: 'rel_m_m',
+        unfilteredLabel: 'All',
+        fetchSelects: createFetchRelated(
+          'dataset',
+          'owners',
+          createErrorHandler(errMsg =>
+            t(
+              'An error occurred while fetching dataset owner values: %s',
+              errMsg,
+            ),
+          ),
+        ),
+        paginate: true,
+      },
+      {
+        Header: t('Datasource'),
+        id: 'database',
+        input: 'select',
+        operator: 'rel_o_m',
+        unfilteredLabel: 'All',
+        fetchSelects: createFetchRelated(
+          'dataset',
+          'database',
+          createErrorHandler(errMsg =>
+            t(
+              'An error occurred while fetching dataset datasource values: %s',
+              errMsg,
+            ),
+          ),
+        ),
+        paginate: true,
+      },
+      {
+        Header: t('Schema'),
+        id: 'schema',
+        input: 'select',
+        operator: 'eq',
+        unfilteredLabel: 'All',
+        fetchSelects: createFetchSchemas(errMsg =>
+          t('An error occurred while fetching schema values: %s', errMsg),
+        ),
+        paginate: true,
+      },
+      {
+        Header: t('Type'),
+        id: 'is_sqllab_view',
+        input: 'select',
+        operator: 'eq',
+        unfilteredLabel: 'All',
+        selects: [
+          { label: 'Virtual', value: true },
+          { label: 'Physical', value: false },
+        ],
+      },
+      {
+        Header: t('Search'),
+        id: 'table_name',
+        input: 'search',
+        operator: 'ct',
+      },
+    ],
+    [],
+  );
 
   const menuData: SubMenuProps = {
     activeChild: 'Datasets',
@@ -460,7 +439,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
   if (canDelete) {
     menuData.secondaryButton = {
       name: t('Bulk Select'),
-      onClick: () => setBulkSelectEnabled(!bulkSelectEnabled),
+      onClick: toggleBulkSelect,
     };
   }
 
@@ -468,60 +447,16 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
     setDatasetCurrentlyDeleting(null);
   };
 
-  const closeDatasetEditModal = () => setDatasetCurrentlyEditing(null);
-
-  const fetchData = useCallback(
-    ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
-      // set loading state, cache the last config for fetching data in this 
component.
-      setLoading(true);
-      setLastFetchDataConfig({
-        filters,
-        pageIndex,
-        pageSize,
-        sortBy,
-      });
-      const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
-        col,
-        opr,
-        value,
-      }));
-
-      const queryParams = rison.encode({
-        order_column: sortBy[0].id,
-        order_direction: sortBy[0].desc ? 'desc' : 'asc',
-        page: pageIndex,
-        page_size: pageSize,
-        ...(filterExps.length ? { filters: filterExps } : {}),
-      });
-
-      return SupersetClient.get({
-        endpoint: `/api/v1/dataset/?q=${queryParams}`,
-      })
-        .then(
-          ({ json }) => {
-            setLoading(false);
-            setDatasets(json.result);
-            setDatasetCount(json.count);
-          },
-          createErrorHandler(errMsg =>
-            addDangerToast(
-              t('An error occurred while fetching datasets: %s', errMsg),
-            ),
-          ),
-        )
-        .finally(() => setLoading(false));
-    },
-    [],
-  );
+  const closeDatasetEditModal = () => {
+    setDatasetCurrentlyEditing(null);
+  };
 
   const handleDatasetDelete = ({ id, table_name: tableName }: Dataset) => {
     SupersetClient.delete({
       endpoint: `/api/v1/dataset/${id}`,
     }).then(
       () => {
-        if (lastFetchDataConfig) {
-          fetchData(lastFetchDataConfig);
-        }
+        refreshData();
         setDatasetCurrentlyDeleting(null);
         addSuccessToast(t('Deleted: %s', tableName));
       },
@@ -533,16 +468,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
     );
   };
 
-  const handleBulkDatasetDelete = () => {
+  const handleBulkDatasetDelete = (datasetsToDelete: Dataset[]) => {
     SupersetClient.delete({
       endpoint: `/api/v1/dataset/?q=${rison.encode(
-        datasets.map(({ id }) => id),
+        datasetsToDelete.map(({ id }) => id),
       )}`,
     }).then(
       ({ json = {} }) => {
-        if (lastFetchDataConfig) {
-          fetchData(lastFetchDataConfig);
-        }
+        refreshData();
         addSuccessToast(json.message);
       },
       createErrorHandler(errMsg =>
@@ -553,21 +486,13 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
     );
   };
 
-  const handleUpdateDataset = () => {
-    if (lastFetchDataConfig) {
-      fetchData(lastFetchDataConfig);
-    }
-  };
-
   return (
     <>
       <SubMenu {...menuData} />
       <AddDatasetModal
         show={datasetAddModalOpen}
         onHide={() => setDatasetAddModalOpen(false)}
-        onDatasetAdd={() => {
-          if (lastFetchDataConfig) fetchData(lastFetchDataConfig);
-        }}
+        onDatasetAdd={refreshData}
       />
       {datasetCurrentlyDeleting && (
         <DeleteModal
@@ -577,12 +502,24 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
             datasetCurrentlyDeleting.chart_count,
             datasetCurrentlyDeleting.dashboard_count,
           )}
-          onConfirm={() => handleDatasetDelete(datasetCurrentlyDeleting)}
+          onConfirm={() => {
+            if (datasetCurrentlyDeleting) {
+              handleDatasetDelete(datasetCurrentlyDeleting);
+            }
+          }}
           onHide={closeDatasetDeleteModal}
           open
           title={t('Delete Dataset?')}
         />
       )}
+      {datasetCurrentlyEditing && (
+        <DatasourceModal
+          datasource={datasetCurrentlyEditing}
+          onDatasourceSave={refreshData}
+          onHide={closeDatasetEditModal}
+          show
+        />
+      )}
       <ConfirmStatusChange
         title={t('Please confirm')}
         description={t(
@@ -603,82 +540,54 @@ const DatasetList: FunctionComponent<DatasetListProps> = 
({
             : [];
 
           return (
-            <>
-              {datasetCurrentlyDeleting && (
-                <DeleteModal
-                  description={t(
-                    `The dataset ${datasetCurrentlyDeleting.table_name} is 
linked to
-                  ${datasetCurrentlyDeleting.chart_count} charts that appear on
-                  ${datasetCurrentlyDeleting.dashboard_count} dashboards.
-                  Are you sure you want to continue? Deleting the dataset will 
break
-                  those objects.`,
-                  )}
-                  onConfirm={() =>
-                    handleDatasetDelete(datasetCurrentlyDeleting)
-                  }
-                  onHide={closeDatasetDeleteModal}
-                  open
-                  title={t('Delete Dataset?')}
-                />
-              )}
-              {datasetCurrentlyEditing && (
-                <DatasourceModal
-                  datasource={datasetCurrentlyEditing}
-                  onDatasourceSave={handleUpdateDataset}
-                  onHide={closeDatasetEditModal}
-                  show
-                />
-              )}
-              <ListView
-                className="dataset-list-view"
-                columns={columns}
-                data={datasets}
-                count={datasetCount}
-                pageSize={PAGE_SIZE}
-                fetchData={fetchData}
-                filters={filterTypes}
-                loading={loading}
-                initialSort={initialSort}
-                bulkActions={bulkActions}
-                bulkSelectEnabled={bulkSelectEnabled}
-                disableBulkSelect={() => setBulkSelectEnabled(false)}
-                renderBulkSelectCopy={selected => {
-                  const { virtualCount, physicalCount } = selected.reduce(
-                    (acc, e) => {
-                      if (e.original.kind === 'physical')
-                        acc.physicalCount += 1;
-                      else if (e.original.kind === 'virtual')
-                        acc.virtualCount += 1;
-                      return acc;
-                    },
-                    { virtualCount: 0, physicalCount: 0 },
+            <ListView
+              className="dataset-list-view"
+              columns={columns}
+              data={datasets}
+              count={datasetCount}
+              pageSize={PAGE_SIZE}
+              fetchData={fetchData}
+              filters={filterTypes}
+              loading={loading}
+              initialSort={initialSort}
+              bulkActions={bulkActions}
+              bulkSelectEnabled={bulkSelectEnabled}
+              disableBulkSelect={toggleBulkSelect}
+              renderBulkSelectCopy={selected => {
+                const { virtualCount, physicalCount } = selected.reduce(
+                  (acc, e) => {
+                    if (e.original.kind === 'physical') acc.physicalCount += 1;
+                    else if (e.original.kind === 'virtual')
+                      acc.virtualCount += 1;
+                    return acc;
+                  },
+                  { virtualCount: 0, physicalCount: 0 },
+                );
+
+                if (!selected.length) {
+                  return t('0 Selected');
+                } else if (virtualCount && !physicalCount) {
+                  return t(
+                    '%s Selected (Virtual)',
+                    selected.length,
+                    virtualCount,
                   );
-
-                  if (!selected.length) {
-                    return t('0 Selected');
-                  } else if (virtualCount && !physicalCount) {
-                    return t(
-                      '%s Selected (Virtual)',
-                      selected.length,
-                      virtualCount,
-                    );
-                  } else if (physicalCount && !virtualCount) {
-                    return t(
-                      '%s Selected (Physical)',
-                      selected.length,
-                      physicalCount,
-                    );
-                  }
-
+                } else if (physicalCount && !virtualCount) {
                   return t(
-                    '%s Selected (%s Physical, %s Virtual)',
+                    '%s Selected (Physical)',
                     selected.length,
                     physicalCount,
-                    virtualCount,
                   );
-                }}
-              />
-            </>
+                }
+
+                return t(
+                  '%s Selected (%s Physical, %s Virtual)',
+                  selected.length,
+                  physicalCount,
+                  virtualCount,
+                );
+              }}
+            />
           );
         }}
       </ConfirmStatusChange>
diff --git a/superset-frontend/src/views/CRUD/hooks.ts 
b/superset-frontend/src/views/CRUD/hooks.ts
new file mode 100644
index 0000000..56e5c56
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -0,0 +1,224 @@
+/**
+ * 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 rison from 'rison';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { SupersetClient } from '@superset-ui/connection';
+import { t } from '@superset-ui/translation';
+
+import { createErrorHandler } from 'src/views/CRUD/utils';
+import { FetchDataConfig } from 'src/components/ListView';
+import { FavoriteStatus } from './types';
+
+interface ListViewResourceState<D extends object = any> {
+  loading: boolean;
+  collection: D[];
+  count: number;
+  permissions: string[];
+  lastFetchDataConfig: FetchDataConfig | null;
+  bulkSelectEnabled: boolean;
+}
+
+export function useListViewResource<D extends object = any>(
+  resource: string,
+  resourceLabel: string, // resourceLabel for translations
+  handleErrorMsg: (errorMsg: string) => void,
+) {
+  const [state, setState] = useState<ListViewResourceState<D>>({
+    count: 0,
+    collection: [],
+    loading: true,
+    lastFetchDataConfig: null,
+    permissions: [],
+    bulkSelectEnabled: false,
+  });
+
+  function updateState(update: Partial<ListViewResourceState<D>>) {
+    setState(currentState => ({ ...currentState, ...update }));
+  }
+
+  function toggleBulkSelect() {
+    updateState({ bulkSelectEnabled: !state.bulkSelectEnabled });
+  }
+
+  useEffect(() => {
+    SupersetClient.get({
+      endpoint: `/api/v1/${resource}/_info`,
+    }).then(
+      ({ json: infoJson = {} }) => {
+        updateState({
+          permissions: infoJson.permissions,
+        });
+      },
+      createErrorHandler(errMsg =>
+        handleErrorMsg(
+          t(
+            'An error occurred while fetching %ss info: %s',
+            resourceLabel,
+            errMsg,
+          ),
+        ),
+      ),
+    );
+  }, []);
+
+  function hasPerm(perm: string) {
+    if (!state.permissions.length) {
+      return false;
+    }
+
+    return Boolean(state.permissions.find(p => p === perm));
+  }
+
+  const fetchData = useCallback(
+    ({
+      pageIndex,
+      pageSize,
+      sortBy,
+      filters: filterValues,
+    }: FetchDataConfig) => {
+      // set loading state, cache the last config for refreshing data.
+      updateState({
+        lastFetchDataConfig: {
+          filters: filterValues,
+          pageIndex,
+          pageSize,
+          sortBy,
+        },
+        loading: true,
+      });
+
+      const filterExps = filterValues.map(
+        ({ id: col, operator: opr, value }) => ({
+          col,
+          opr,
+          value,
+        }),
+      );
+
+      const queryParams = rison.encode({
+        order_column: sortBy[0].id,
+        order_direction: sortBy[0].desc ? 'desc' : 'asc',
+        page: pageIndex,
+        page_size: pageSize,
+        ...(filterExps.length ? { filters: filterExps } : {}),
+      });
+
+      return SupersetClient.get({
+        endpoint: `/api/v1/${resource}/?q=${queryParams}`,
+      })
+        .then(
+          ({ json = {} }) => {
+            updateState({
+              collection: json.result,
+              count: json.count,
+            });
+          },
+          createErrorHandler(errMsg =>
+            handleErrorMsg(
+              t(
+                'An error occurred while fetching %ss: %s',
+                resourceLabel,
+                errMsg,
+              ),
+            ),
+          ),
+        )
+        .finally(() => {
+          updateState({ loading: false });
+        });
+    },
+    [],
+  );
+
+  return {
+    state: {
+      loading: state.loading,
+      resourceCount: state.count,
+      resourceCollection: state.collection,
+      bulkSelectEnabled: state.bulkSelectEnabled,
+    },
+    setResourceCollection: (update: D[]) =>
+      updateState({
+        collection: update,
+      }),
+    hasPerm,
+    fetchData,
+    toggleBulkSelect,
+    refreshData: () => {
+      if (state.lastFetchDataConfig) {
+        fetchData(state.lastFetchDataConfig);
+      }
+    },
+  };
+}
+
+// the hooks api has some known limitations around stale state in closures.
+// See 
https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks
+// the useRef hook is a way of getting around these limitations by having a 
consistent ref
+// that points to the most recent value.
+export function useFavoriteStatus(
+  initialState: FavoriteStatus,
+  baseURL: string,
+  handleErrorMsg: (message: string) => void,
+) {
+  const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>(
+    initialState,
+  );
+  const favoriteStatusRef = useRef<FavoriteStatus>(favoriteStatus);
+  useEffect(() => {
+    favoriteStatusRef.current = favoriteStatus;
+  });
+
+  const updateFavoriteStatus = (update: FavoriteStatus) =>
+    setFavoriteStatus(currentState => ({ ...currentState, ...update }));
+
+  const fetchFaveStar = (id: number) => {
+    SupersetClient.get({
+      endpoint: `${baseURL}/${id}/count/`,
+    }).then(
+      ({ json }) => {
+        updateFavoriteStatus({ [id]: json.count > 0 });
+      },
+      createErrorHandler(errMsg =>
+        handleErrorMsg(
+          t('There was an error fetching the favorite status: %s', errMsg),
+        ),
+      ),
+    );
+  };
+
+  const saveFaveStar = (id: number, isStarred: boolean) => {
+    const urlSuffix = isStarred ? 'unselect' : 'select';
+
+    SupersetClient.get({
+      endpoint: `${baseURL}/${id}/${urlSuffix}/`,
+    }).then(
+      () => {
+        updateFavoriteStatus({ [id]: !isStarred });
+      },
+      createErrorHandler(errMsg =>
+        handleErrorMsg(
+          t('There was an error saving the favorite status: %s', errMsg),
+        ),
+      ),
+    );
+  };
+
+  return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const;
+}
diff --git a/superset-frontend/src/views/CRUD/types.ts 
b/superset-frontend/src/views/CRUD/types.ts
new file mode 100644
index 0000000..91d88a3
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+export type FavoriteStatus = {
+  [id: number]: boolean;
+};
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
index 5aea548..2bced46 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -20,7 +20,6 @@ import {
   SupersetClient,
   SupersetClientResponse,
 } from '@superset-ui/connection';
-import { t } from '@superset-ui/translation';
 import rison from 'rison';
 import getClientErrorObject from 'src/utils/getClientErrorObject';
 import { logging } from '@superset-ui/core';
@@ -58,59 +57,6 @@ export function createErrorHandler(handleErrorFunc: 
(errMsg?: string) => void) {
   return async (e: SupersetClientResponse | string) => {
     const parsedError = await getClientErrorObject(e);
     logging.error(e);
-    handleErrorFunc(parsedError.message);
-  };
-}
-
-export function createFaveStarHandlers(
-  baseURL: string,
-  context: any,
-  handleErrorFunc: (message: string) => void,
-) {
-  const fetchFaveStar = (id: number) => {
-    SupersetClient.get({
-      endpoint: `${baseURL}/${id}/count/`,
-    })
-      .then(({ json }) => {
-        const faves = {
-          ...context.state.favoriteStatus,
-        };
-
-        faves[id] = json.count > 0;
-
-        context.setState({
-          favoriteStatus: faves,
-        });
-      })
-      .catch(() =>
-        handleErrorFunc(t('There was an error fetching the favorite status')),
-      );
-  };
-
-  const saveFaveStar = (id: number, isStarred: boolean) => {
-    const urlSuffix = isStarred ? 'unselect' : 'select';
-
-    SupersetClient.get({
-      endpoint: `${baseURL}/${id}/${urlSuffix}/`,
-    })
-      .then(() => {
-        const faves = {
-          ...context.state.favoriteStatus,
-        };
-
-        faves[id] = !isStarred;
-
-        context.setState({
-          favoriteStatus: faves,
-        });
-      })
-      .catch(() =>
-        handleErrorFunc(t('There was an error saving the favorite status')),
-      );
-  };
-
-  return {
-    fetchFaveStar,
-    saveFaveStar,
+    handleErrorFunc(parsedError.error);
   };
 }

Reply via email to