From 7b3f4fe64cac2e4f372fca019af0f03be38ec8f6 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 19:57:46 +0200 Subject: [PATCH 01/12] Optimize ListToolbar --- .../ra-core/src/controller/ListController.tsx | 15 +++++--- .../ra-core/src/controller/useListParams.ts | 34 +++++++++++------- packages/ra-core/src/util/index.ts | 2 ++ .../ra-core/src/util/useWhyDidYouUpdate.ts | 35 +++++++++++++++++++ .../ra-ui-materialui/src/list/ListToolbar.js | 6 ++-- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 packages/ra-core/src/util/useWhyDidYouUpdate.ts diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 907bddd1c9b..2b0942ad1a6 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -1,4 +1,4 @@ -import { isValidElement, ReactNode, ReactElement } from 'react'; +import { isValidElement, ReactNode, ReactElement, useMemo } from 'react'; import inflection from 'inflection'; import { SORT_ASC } from '../reducer/admin/resource/list/queryReducer'; @@ -172,6 +172,14 @@ const ListController = (props: Props) => { queryModifiers.setPage(query.page - 1); } + const currentSort = useMemo( + () => ({ + field: query.sort, + order: query.order, + }), + [query.sort, query.order] + ); + const resourceName = translate(`resources.${resource}.name`, { smart_count: 2, _: inflection.humanize(inflection.pluralize(resource)), @@ -182,10 +190,7 @@ const ListController = (props: Props) => { return children({ basePath, - currentSort: { - field: query.sort, - order: query.order, - }, + currentSort, data, defaultTitle, displayedFilters: query.displayedFilters, diff --git a/packages/ra-core/src/controller/useListParams.ts b/packages/ra-core/src/controller/useListParams.ts index 1ae1eebb43f..2399459f6c1 100644 --- a/packages/ra-core/src/controller/useListParams.ts +++ b/packages/ra-core/src/controller/useListParams.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useState, useMemo } from 'react'; // @ts-ignore import { useSelector, useDispatch } from 'react-redux'; import { parse, stringify } from 'query-string'; @@ -47,6 +47,13 @@ interface Modifiers { showFilter: (filterName: string, defaultValue: any) => void; } +const emptyObject = {}; + +const defaultSort = { + field: 'id', + order: SORT_ASC, +}; + /** * Get the list parameters (page, sort, filters) and modifiers. * @@ -100,10 +107,7 @@ const useListParams = ({ resource, location, filterDefaultValues, - sort = { - field: 'id', - order: SORT_ASC, - }, + sort = defaultSort, perPage = 10, debounce = 500, }: ListParamsOptions): [Parameters, Modifiers] => { @@ -115,13 +119,17 @@ const useListParams = ({ [resource] ); - const query = getQuery({ - location, - params, - filterDefaultValues, - sort, - perPage, - }); + const query = useMemo( + () => + getQuery({ + location, + params, + filterDefaultValues, + sort, + perPage, + }), + [location.search, params, filterDefaultValues, sort, perPage] + ); const requestSignature = [resource, JSON.stringify(query)]; @@ -153,7 +161,7 @@ const useListParams = ({ requestSignature ); - const filterValues = query.filter || {}; + const filterValues = query.filter || emptyObject; const setFilters = useCallback( lodashDebounce(filters => { diff --git a/packages/ra-core/src/util/index.ts b/packages/ra-core/src/util/index.ts index 1213f70929c..7f11314d11e 100644 --- a/packages/ra-core/src/util/index.ts +++ b/packages/ra-core/src/util/index.ts @@ -10,6 +10,7 @@ import resolveRedirectTo from './resolveRedirectTo'; import TestContext from './TestContext'; import renderWithRedux from './renderWithRedux'; import warning from './warning'; +import useWhyDidYouUpdate from './useWhyDidYouUpdate'; export { downloadCSV, @@ -24,4 +25,5 @@ export { TestContext, renderWithRedux, warning, + useWhyDidYouUpdate, }; diff --git a/packages/ra-core/src/util/useWhyDidYouUpdate.ts b/packages/ra-core/src/util/useWhyDidYouUpdate.ts new file mode 100644 index 00000000000..311b0e401be --- /dev/null +++ b/packages/ra-core/src/util/useWhyDidYouUpdate.ts @@ -0,0 +1,35 @@ +import { useRef, useEffect } from 'react'; + +export default function useWhyDidYouUpdate(name, props) { + // Get a mutable ref object where we can store props ... + // ... for comparison next time this hook runs. + const previousProps = useRef(); + + useEffect(() => { + if (previousProps.current) { + // Get all keys from previous and current props + const allKeys = Object.keys({ ...previousProps.current, ...props }); + // Use this object to keep track of changed props + const changesObj = {}; + // Iterate through keys + allKeys.forEach(key => { + // If previous is different from current + if (previousProps.current[key] !== props[key]) { + // Add to changesObj + changesObj[key] = { + from: previousProps.current[key], + to: props[key], + }; + } + }); + + // If changesObj not empty then output to console + if (Object.keys(changesObj).length) { + console.log('[why-did-you-update]', name, changesObj); + } + } + + // Finally update previousProps with current props for next hook call + previousProps.current = props; + }); +} diff --git a/packages/ra-ui-materialui/src/list/ListToolbar.js b/packages/ra-ui-materialui/src/list/ListToolbar.js index 41904569e96..a1be2c08d25 100644 --- a/packages/ra-ui-materialui/src/list/ListToolbar.js +++ b/packages/ra-ui-materialui/src/list/ListToolbar.js @@ -26,8 +26,10 @@ const useStyles = makeStyles(theme => ({ }, })); +const defaultClasses = {}; // avoid needless updates + const ListToolbar = ({ - classes = {}, + classes = defaultClasses, filters, filterValues, // dynamically set via the UI by the user permanentFilter, // set in the List component by the developer @@ -70,4 +72,4 @@ ListToolbar.propTypes = { exporter: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), }; -export default ListToolbar; +export default React.memo(ListToolbar); From 900e1601adc984034e6323c171d9747076eb8e63 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 19:58:04 +0200 Subject: [PATCH 02/12] Hookify Responsive --- .../ra-ui-materialui/src/layout/Responsive.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ra-ui-materialui/src/layout/Responsive.js b/packages/ra-ui-materialui/src/layout/Responsive.js index e5961424642..d665e261388 100644 --- a/packages/ra-ui-materialui/src/layout/Responsive.js +++ b/packages/ra-ui-materialui/src/layout/Responsive.js @@ -1,15 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import withWidth from '@material-ui/core/withWidth'; +import { useTheme } from '@material-ui/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; -export const Responsive = ({ - xsmall, - small, - medium, - large, - width, - ...rest -}) => { +export const Responsive = ({ xsmall, small, medium, large, ...rest }) => { + const theme = useTheme(); + const width = + [...theme.breakpoints.keys].reverse().reduce((output, key) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const matches = useMediaQuery(theme.breakpoints.only(key)); + return !output && matches ? key : output; + }, null) || 'xs'; let element; switch (width) { case 'xs': @@ -59,7 +60,6 @@ Responsive.propTypes = { small: PropTypes.element, medium: PropTypes.element, large: PropTypes.element, - width: PropTypes.string, }; -export default withWidth()(Responsive); +export default Responsive; From 96745017b8216172bc5e559a458a21539f45da53 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 19:58:23 +0200 Subject: [PATCH 03/12] Fix review detail paper zIndex --- examples/demo/src/reviews/ReviewList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/demo/src/reviews/ReviewList.js b/examples/demo/src/reviews/ReviewList.js index 6b4110852a1..5e265811969 100644 --- a/examples/demo/src/reviews/ReviewList.js +++ b/examples/demo/src/reviews/ReviewList.js @@ -37,6 +37,9 @@ const styles = theme => listWithDrawer: { marginRight: 400, }, + drawerPaper: { + zIndex: 100, + }, }); class ReviewList extends Component { From 4ead9466eb4110a285a74cf57dc81eb71a51beed Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 21:02:38 +0200 Subject: [PATCH 04/12] Fix default value varying in ListController --- packages/ra-core/src/controller/ListController.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 2b0942ad1a6..19676aa0d81 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -65,6 +65,10 @@ interface Props { [key: string]: any; } +const defaultSort = { + field: 'id', + order: SORT_ASC, +}; /** * List page component * @@ -125,10 +129,7 @@ const ListController = (props: Props) => { hasCreate, location, filterDefaultValues, - sort = { - field: 'id', - order: SORT_ASC, - }, + sort = defaultSort, perPage = 10, filter, debounce = 500, From 864075c619d57a08b9101810d2bb0bf8d4c1f8d4 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 21:02:59 +0200 Subject: [PATCH 05/12] Hookify DatagridBody and ListActions --- .../ra-ui-materialui/src/list/DatagridBody.js | 87 +++++++++---------- .../ra-ui-materialui/src/list/ListActions.js | 75 ++++++++-------- 2 files changed, 78 insertions(+), 84 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/DatagridBody.js b/packages/ra-ui-materialui/src/list/DatagridBody.js index 7e10981b0e0..e75ac20a31b 100644 --- a/packages/ra-ui-materialui/src/list/DatagridBody.js +++ b/packages/ra-ui-materialui/src/list/DatagridBody.js @@ -1,6 +1,5 @@ -import React, { cloneElement } from 'react'; +import React, { cloneElement, useMemo } from 'react'; import PropTypes from 'prop-types'; -import shouldUpdate from 'recompose/shouldUpdate'; import TableBody from '@material-ui/core/TableBody'; import classnames from 'classnames'; @@ -26,36 +25,45 @@ const DatagridBody = ({ styles, version, ...rest -}) => ( - - {ids.map((id, rowIndex) => - cloneElement( - row, - { - basePath, - classes, - className: classnames(classes.row, { - [classes.rowEven]: rowIndex % 2 === 0, - [classes.rowOdd]: rowIndex % 2 !== 0, - [classes.clickableRow]: rowClick, - }), - expand, - hasBulkActions, - hover, - id, - key: id, - onToggleItem, - record: data[id], - resource, - rowClick, - selected: selectedIds.includes(id), - style: rowStyle ? rowStyle(data[id], rowIndex) : null, - }, - children - ) - )} - -); +}) => + useMemo( + () => ( + + {ids.map((id, rowIndex) => + cloneElement( + row, + { + basePath, + classes, + className: classnames(classes.row, { + [classes.rowEven]: rowIndex % 2 === 0, + [classes.rowOdd]: rowIndex % 2 !== 0, + [classes.clickableRow]: rowClick, + }), + expand, + hasBulkActions, + hover, + id, + key: id, + onToggleItem, + record: data[id], + resource, + rowClick, + selected: selectedIds.includes(id), + style: rowStyle + ? rowStyle(data[id], rowIndex) + : null, + }, + children + ) + )} + + ), + [version, isLoading, data, JSON.stringify(ids)] + ); DatagridBody.propTypes = { basePath: PropTypes.string, @@ -85,19 +93,8 @@ DatagridBody.defaultProps = { row: , }; -const areArraysEqual = (arr1, arr2) => - arr1.length == arr2.length && arr1.every((v, i) => v === arr2[i]); - -const PureDatagridBody = shouldUpdate( - (props, nextProps) => - props.version !== nextProps.version || - nextProps.isLoading === false || - !areArraysEqual(props.ids, nextProps.ids) || - props.data !== nextProps.data -)(DatagridBody); - // trick material-ui Table into thinking this is one of the child type it supports // @ts-ignore -PureDatagridBody.muiName = 'TableBody'; +DatagridBody.muiName = 'TableBody'; -export default PureDatagridBody; +export default DatagridBody; diff --git a/packages/ra-ui-materialui/src/list/ListActions.js b/packages/ra-ui-materialui/src/list/ListActions.js index 64a70ad55d2..13f144234f6 100644 --- a/packages/ra-ui-materialui/src/list/ListActions.js +++ b/packages/ra-ui-materialui/src/list/ListActions.js @@ -1,6 +1,5 @@ -import React, { cloneElement } from 'react'; +import React, { cloneElement, useMemo } from 'react'; import PropTypes from 'prop-types'; -import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; import { sanitizeListRestProps } from 'ra-core'; import TopToolbar from '../layout/TopToolbar'; @@ -23,36 +22,40 @@ const ListActions = ({ showFilter, total, ...rest -}) => ( - - {bulkActions && - cloneElement(bulkActions, { - basePath, - filterValues, - resource, - selectedIds, - onUnselectItems, - })} - {filters && - cloneElement(filters, { - resource, - showFilter, - displayedFilters, - filterValues, - context: 'button', - })} - {hasCreate && } - {exporter !== false && ( - - )} - -); +}) => + useMemo( + () => ( + + {bulkActions && + cloneElement(bulkActions, { + basePath, + filterValues, + resource, + selectedIds, + onUnselectItems, + })} + {filters && + cloneElement(filters, { + resource, + showFilter, + displayedFilters, + filterValues, + context: 'button', + })} + {hasCreate && } + {exporter !== false && ( + + )} + + ), + [resource, displayedFilters, filterValues, selectedIds, filters] + ); ListActions.propTypes = { bulkActions: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]), @@ -76,10 +79,4 @@ ListActions.defaultProps = { onUnselectItems: () => null, }; -export default onlyUpdateForKeys([ - 'resource', - 'filters', - 'displayedFilters', - 'filterValues', - 'selectedIds', -])(ListActions); +export default ListActions; From 562a73df845615501baed5faf73f238a6a102816 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 21:08:11 +0200 Subject: [PATCH 06/12] compute cache key once --- packages/ra-core/src/controller/useListParams.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ra-core/src/controller/useListParams.ts b/packages/ra-core/src/controller/useListParams.ts index 2399459f6c1..205e7d6f8bc 100644 --- a/packages/ra-core/src/controller/useListParams.ts +++ b/packages/ra-core/src/controller/useListParams.ts @@ -119,6 +119,15 @@ const useListParams = ({ [resource] ); + const requestSignature = [ + location.search, + resource, + params, + filterDefaultValues, + JSON.stringify(sort), + perPage, + ]; + const query = useMemo( () => getQuery({ @@ -128,11 +137,9 @@ const useListParams = ({ sort, perPage, }), - [location.search, params, filterDefaultValues, sort, perPage] + requestSignature ); - const requestSignature = [resource, JSON.stringify(query)]; - const changeParams = useCallback(action => { const newParams = queryReducer(query, action); dispatch( From 5c3e7e1d0cb466662c91f482d18ea1765c49eb75 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 21:08:23 +0200 Subject: [PATCH 07/12] fix typescript --- packages/ra-core/src/util/useWhyDidYouUpdate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/util/useWhyDidYouUpdate.ts b/packages/ra-core/src/util/useWhyDidYouUpdate.ts index 311b0e401be..86bd8d28653 100644 --- a/packages/ra-core/src/util/useWhyDidYouUpdate.ts +++ b/packages/ra-core/src/util/useWhyDidYouUpdate.ts @@ -3,7 +3,7 @@ import { useRef, useEffect } from 'react'; export default function useWhyDidYouUpdate(name, props) { // Get a mutable ref object where we can store props ... // ... for comparison next time this hook runs. - const previousProps = useRef(); + const previousProps = useRef() as any; useEffect(() => { if (previousProps.current) { From 1346f5e44798583f02a978489f9489bd2243cf39 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 21:17:55 +0200 Subject: [PATCH 08/12] Add missing reference of hook source --- packages/ra-core/src/util/useWhyDidYouUpdate.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ra-core/src/util/useWhyDidYouUpdate.ts b/packages/ra-core/src/util/useWhyDidYouUpdate.ts index 86bd8d28653..11d865e8a44 100644 --- a/packages/ra-core/src/util/useWhyDidYouUpdate.ts +++ b/packages/ra-core/src/util/useWhyDidYouUpdate.ts @@ -1,5 +1,16 @@ import { useRef, useEffect } from 'react'; +/** + * Debug hook showing which props updated between two renders + * @example + * + * const MyComponent = React.memo(props => { + * useWhyDidYouUpdate('MyComponent', props); + * return Date: Fri, 7 Jun 2019 22:04:43 +0200 Subject: [PATCH 09/12] Revert "Hookify Responsive" This reverts commit 900e1601adc984034e6323c171d9747076eb8e63. --- .../ra-ui-materialui/src/layout/Responsive.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ra-ui-materialui/src/layout/Responsive.js b/packages/ra-ui-materialui/src/layout/Responsive.js index d665e261388..e5961424642 100644 --- a/packages/ra-ui-materialui/src/layout/Responsive.js +++ b/packages/ra-ui-materialui/src/layout/Responsive.js @@ -1,16 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useTheme } from '@material-ui/styles'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; +import withWidth from '@material-ui/core/withWidth'; -export const Responsive = ({ xsmall, small, medium, large, ...rest }) => { - const theme = useTheme(); - const width = - [...theme.breakpoints.keys].reverse().reduce((output, key) => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const matches = useMediaQuery(theme.breakpoints.only(key)); - return !output && matches ? key : output; - }, null) || 'xs'; +export const Responsive = ({ + xsmall, + small, + medium, + large, + width, + ...rest +}) => { let element; switch (width) { case 'xs': @@ -60,6 +59,7 @@ Responsive.propTypes = { small: PropTypes.element, medium: PropTypes.element, large: PropTypes.element, + width: PropTypes.string, }; -export default Responsive; +export default withWidth()(Responsive); From 16a1fea47041048d9679e0853ac4a7f215e3f882 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 22:52:04 +0200 Subject: [PATCH 10/12] Fix row selection --- packages/ra-ui-materialui/src/list/DatagridBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/list/DatagridBody.js b/packages/ra-ui-materialui/src/list/DatagridBody.js index e75ac20a31b..3212ed32182 100644 --- a/packages/ra-ui-materialui/src/list/DatagridBody.js +++ b/packages/ra-ui-materialui/src/list/DatagridBody.js @@ -62,7 +62,7 @@ const DatagridBody = ({ )} ), - [version, isLoading, data, JSON.stringify(ids)] + [version, isLoading, data, selectedIds, JSON.stringify(ids)] ); DatagridBody.propTypes = { From 23b806812c7b1f6fed2b499b6929ff0646cbb7db Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 22:52:29 +0200 Subject: [PATCH 11/12] Fix list appears loaded when at least one record was fetched --- packages/ra-core/src/fetch/useGetList.ts | 8 ++++---- packages/ra-core/src/fetch/useQueryWithStore.ts | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ra-core/src/fetch/useGetList.ts b/packages/ra-core/src/fetch/useGetList.ts index 69d91581b27..aa9930c23c1 100644 --- a/packages/ra-core/src/fetch/useGetList.ts +++ b/packages/ra-core/src/fetch/useGetList.ts @@ -50,22 +50,22 @@ const useGetList = ( filter: object, options?: any ) => { - const { data, total, error, loading, loaded } = useQueryWithStore( + const { data: ids, total, error, loading, loaded } = useQueryWithStore( { type: GET_LIST, resource, payload: { pagination, sort, filter } }, { ...options, action: CRUD_GET_LIST }, (state: ReduxState) => state.admin.resources[resource] - ? state.admin.resources[resource].data + ? state.admin.resources[resource].list.ids : null, (state: ReduxState) => state.admin.resources[resource] ? state.admin.resources[resource].list.total : null ); - const ids = useSelector( + const data = useSelector( (state: ReduxState) => state.admin.resources[resource] - ? state.admin.resources[resource].list.ids + ? state.admin.resources[resource].data : null, [resource] ); diff --git a/packages/ra-core/src/fetch/useQueryWithStore.ts b/packages/ra-core/src/fetch/useQueryWithStore.ts index ed9f9579b63..954a68be61e 100644 --- a/packages/ra-core/src/fetch/useQueryWithStore.ts +++ b/packages/ra-core/src/fetch/useQueryWithStore.ts @@ -25,7 +25,11 @@ export interface QueryOptions { * @see src/reducer/admin/data.ts */ const isEmptyList = data => - data && Object.keys(data).length === 0 && data.hasOwnProperty('fetchedAt'); + Array.isArray(data) + ? data.length === 0 + : data && + Object.keys(data).length === 0 && + data.hasOwnProperty('fetchedAt'); /** * Default cache selector. Allows to cache responses by default. From f672d3788fcc561ebc0583194ac38e47d5447195 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 7 Jun 2019 22:56:06 +0200 Subject: [PATCH 12/12] Fix propTypes warnings --- packages/ra-ui-materialui/src/list/DatagridBody.js | 2 +- packages/ra-ui-materialui/src/list/ListActions.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-ui-materialui/src/list/DatagridBody.js b/packages/ra-ui-materialui/src/list/DatagridBody.js index 3212ed32182..5220e70369c 100644 --- a/packages/ra-ui-materialui/src/list/DatagridBody.js +++ b/packages/ra-ui-materialui/src/list/DatagridBody.js @@ -81,7 +81,7 @@ DatagridBody.propTypes = { row: PropTypes.element, rowClick: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), rowStyle: PropTypes.func, - selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired, + selectedIds: PropTypes.arrayOf(PropTypes.any), styles: PropTypes.object, version: PropTypes.number, }; diff --git a/packages/ra-ui-materialui/src/list/ListActions.js b/packages/ra-ui-materialui/src/list/ListActions.js index 13f144234f6..52c965ba35c 100644 --- a/packages/ra-ui-materialui/src/list/ListActions.js +++ b/packages/ra-ui-materialui/src/list/ListActions.js @@ -71,7 +71,7 @@ ListActions.propTypes = { onUnselectItems: PropTypes.func.isRequired, selectedIds: PropTypes.arrayOf(PropTypes.any), showFilter: PropTypes.func, - total: PropTypes.number.isRequired, + total: PropTypes.number, }; ListActions.defaultProps = {