From a4d1e4af05b80bb3f503168978bd93c42e0e7249 Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Thu, 26 Aug 2021 01:16:03 +0300 Subject: [PATCH 1/4] Use `reselect` to select file by ids --- src/components/DeleteDialog.jsx | 7 ++++--- src/components/DeleteImmediatelyDialog.jsx | 7 ++++--- src/components/MoveDialog.jsx | 16 +++++++++++----- src/store/reducers/files.js | 13 ++++++++++++- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/components/DeleteDialog.jsx b/src/components/DeleteDialog.jsx index ee435297..748b86ed 100644 --- a/src/components/DeleteDialog.jsx +++ b/src/components/DeleteDialog.jsx @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { moveToTrashBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { getFilesByIds } from '../store/reducers/files'; +import { makeGetFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -31,7 +31,8 @@ function DeleteDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.movingToTrash)); const fileIds = dialogProps.fileIds ?? []; - const files = useSelector((state) => getFilesByIds(state, fileIds)); + const getFilesByIds = makeGetFilesByIds(); + const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onDelete = () => { dispatch(moveToTrashBatch(files.map((file) => file.path))); diff --git a/src/components/DeleteImmediatelyDialog.jsx b/src/components/DeleteImmediatelyDialog.jsx index 5a8a9269..7a334254 100644 --- a/src/components/DeleteImmediatelyDialog.jsx +++ b/src/components/DeleteImmediatelyDialog.jsx @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { deleteImmediatelyBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { getFilesByIds } from '../store/reducers/files'; +import { makeGetFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -31,7 +31,8 @@ function DeleteDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.deletingFileImmediately)); const fileIds = dialogProps.fileIds ?? []; - const files = useSelector((state) => getFilesByIds(state, fileIds)); + const getFilesByIds = makeGetFilesByIds(); + const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onDelete = () => { dispatch(deleteImmediatelyBatch(files.map((file) => file.path))); diff --git a/src/components/MoveDialog.jsx b/src/components/MoveDialog.jsx index 3d030569..6cbbf719 100644 --- a/src/components/MoveDialog.jsx +++ b/src/components/MoveDialog.jsx @@ -1,13 +1,13 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { moveFileBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { getFilesByIds } from '../store/reducers/files'; +import { makeGetFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -32,7 +32,8 @@ function MoveDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.movingFile)); const fileIds = dialogProps.fileIds ?? []; - const files = useSelector((state) => getFilesByIds(state, fileIds)); + const getFilesByIds = makeGetFilesByIds(); + const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onMove = () => { dispatch(moveFileBatch(files.map((file) => ({ @@ -46,6 +47,11 @@ function MoveDialog({ uid }) { dispatch(closeDialog(uid)); }; + const onPathChange = useCallback( + (path) => setToPath(path), + [setToPath], + ); + return ( setToPath(path)} + onPathChange={onPathChange} excludeIds={fileIds} /> diff --git a/src/store/reducers/files.js b/src/store/reducers/files.js index 7ec7318c..000167d3 100644 --- a/src/store/reducers/files.js +++ b/src/store/reducers/files.js @@ -172,7 +172,6 @@ export default combineReducers({ const FILES_EMPTY = []; export const getFileById = (state, id) => state.files.byId[id]; -export const getFilesByIds = (state, ids) => ids.map((id) => getFileById(state, id)); export const getFilesByPath = (state, path) => state.files.byPath[path] || FILES_EMPTY; export const getFilesCountByPath = (state, path) => getFilesByPath(state, path).length; export const getIsFileSelected = (state, id) => state.files.selectedIds.has(id); @@ -186,6 +185,18 @@ export const getThumbnailById = (state, id) => state.files.thumbnailsById[id]; export const getDownloads = (state) => state.files.downloads; +export const makeGetFilesByIds = () => ( + createSelector( + [ + (state) => state.files.byId, + (_state, props) => props.ids, + ], + (byId, ids) => ( + ids.map((id) => byId[id]) + ), + ) +); + export const makeGetPreview = () => ( createSelector( [ From e4f95df5bb2a2e50e7dcd61ed4401b890a13b539 Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Thu, 26 Aug 2021 17:49:04 +0300 Subject: [PATCH 2/4] Refactor `FolderPicker` (#18) --- src/components/FolderPicker.jsx | 30 +++++++++++++++++++----------- src/components/MoveDialog.jsx | 4 ++-- src/containers/FileTableView.js | 4 ++-- src/containers/FolderPicker.js | 33 --------------------------------- src/store/reducers/files.js | 27 ++++++++++++++++++++++++--- src/store/sagas/fileWatchers.js | 8 ++++---- 6 files changed, 51 insertions(+), 55 deletions(-) delete mode 100644 src/containers/FolderPicker.js diff --git a/src/components/FolderPicker.jsx b/src/components/FolderPicker.jsx index 992099f0..862f4ccb 100644 --- a/src/components/FolderPicker.jsx +++ b/src/components/FolderPicker.jsx @@ -1,6 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +import { listFolder } from '../store/actions/files'; +import { scopes } from '../store/actions/loading'; +import { getFolderIdsByPath } from '../store/reducers/files'; +import { getLoading } from '../store/reducers/loading'; + import * as icons from '../icons'; import * as routes from '../routes'; @@ -21,12 +28,16 @@ const changePath = (route, onPathChange) => (event) => { onPathChange(routes.makePathFromUrl(route)); }; -const FolderPicker = React.memo(({ - items, loading, path, listFolder, onPathChange, -}) => { +const FolderPicker = ({ excludeIds, path, onPathChange }) => { + let items = useSelector((state) => getFolderIdsByPath(state, { path })); + const loading = useSelector((state) => getLoading(state, scopes.listingFolder)); + React.useEffect(() => { listFolder(path); - }, [path, listFolder]); + }, [path]); + + const idsToExclude = new Set(excludeIds); + items = items.filter((id) => !idsToExclude.has(id)); return ( <> @@ -85,17 +96,14 @@ const FolderPicker = React.memo(({ ); -}); +}; FolderPicker.propTypes = { - items: PropTypes.arrayOf(PropTypes.string).isRequired, - loading: PropTypes.bool, + excludeIds: PropTypes.arrayOf( + PropTypes.string.isRequired, + ).isRequired, path: PropTypes.string.isRequired, onPathChange: PropTypes.func.isRequired, }; -FolderPicker.defaultProps = { - loading: true, -}; - export default FolderPicker; diff --git a/src/components/MoveDialog.jsx b/src/components/MoveDialog.jsx index 6cbbf719..847e18b9 100644 --- a/src/components/MoveDialog.jsx +++ b/src/components/MoveDialog.jsx @@ -14,10 +14,10 @@ import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; import pluralize from '../pluralize'; import * as routes from '../routes'; -import FolderPicker from '../containers/FolderPicker'; - import Dialog from './ui/Dialog'; +import FolderPicker from './FolderPicker'; + const styles = { height: '40vh', }; diff --git a/src/containers/FileTableView.js b/src/containers/FileTableView.js index f1fe5d81..1c956f4a 100644 --- a/src/containers/FileTableView.js +++ b/src/containers/FileTableView.js @@ -2,14 +2,14 @@ import { connect } from 'react-redux'; import { scopes } from '../store/actions/loading'; -import { getFilesByPath } from '../store/reducers/files'; +import { getFileIdsByPath } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import FileTableView from '../components/FileTableView'; export default connect( (state, ownProps) => ({ - items: getFilesByPath(state, ownProps.path), + items: getFileIdsByPath(state, ownProps.path), loading: getLoading(state, scopes.listingFolder), }), )(FileTableView); diff --git a/src/containers/FolderPicker.js b/src/containers/FolderPicker.js deleted file mode 100644 index bf7307a7..00000000 --- a/src/containers/FolderPicker.js +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from 'react-redux'; - -import { listFolder } from '../store/actions/files'; -import { scopes } from '../store/actions/loading'; - -import { getFilesByPath } from '../store/reducers/files'; -import { getLoading } from '../store/reducers/loading'; - -import FolderPicker from '../components/FolderPicker'; - -function getFilesByPathExclude(state, props) { - const { path } = props; - const excludeIds = new Set(props.excludeIds); - const files = getFilesByPath(state, path); - if (excludeIds == null) { - return files; - } - const nextFiles = files.filter((fileId) => !excludeIds.has(fileId)); - if (files.length === nextFiles.length) { - return files; // return object from state to prevent re-renders - } - return nextFiles; -} - -export default connect( - (state, ownProps) => ({ - items: getFilesByPathExclude(state, ownProps), - loading: getLoading(state, scopes.listingFolder), - }), - { - listFolder, - }, -)(FolderPicker); diff --git a/src/store/reducers/files.js b/src/store/reducers/files.js index 000167d3..591c1091 100644 --- a/src/store/reducers/files.js +++ b/src/store/reducers/files.js @@ -1,6 +1,7 @@ import { combineReducers } from 'redux'; import { createSelector } from 'reselect'; +import { MediaType } from '../../constants'; import * as routes from '../../routes'; import { difference } from '../../set'; @@ -172,8 +173,8 @@ export default combineReducers({ const FILES_EMPTY = []; export const getFileById = (state, id) => state.files.byId[id]; -export const getFilesByPath = (state, path) => state.files.byPath[path] || FILES_EMPTY; -export const getFilesCountByPath = (state, path) => getFilesByPath(state, path).length; +export const getFileIdsByPath = (state, path) => state.files.byPath[path] || FILES_EMPTY; +export const getFilesCountByPath = (state, path) => getFileIdsByPath(state, path).length; export const getIsFileSelected = (state, id) => state.files.selectedIds.has(id); export const getSelectedFileIds = (state) => [...state.files.selectedIds]; export const getSelectedFiles = (state) => ( @@ -185,6 +186,26 @@ export const getThumbnailById = (state, id) => state.files.thumbnailsById[id]; export const getDownloads = (state) => state.files.downloads; +function createPropsSelector(selector) { + return (_, props) => selector(props); +} + +const getPathProp = createPropsSelector((props) => props.path); + +export const getFolderIdsByPath = createSelector( + [ + (state) => state.files.byId, + (state) => state.files.byPath, + getPathProp, + ], + (byId, byPath, path) => ( + (byPath[path] || FILES_EMPTY) + .map((id) => byId[id]) + .filter((item) => item.mediatype === MediaType.FOLDER) + .map((item) => item.id) + ), +); + export const makeGetFilesByIds = () => ( createSelector( [ @@ -201,7 +222,7 @@ export const makeGetPreview = () => ( createSelector( [ (state) => state.files.byId, - (state, props) => getFilesByPath(state, props.dirPath), + (state, props) => getFileIdsByPath(state, props.dirPath), (_state, props) => props.name, ], (byId, files, name) => { diff --git a/src/store/sagas/fileWatchers.js b/src/store/sagas/fileWatchers.js index 2a647c10..e9b14aed 100644 --- a/src/store/sagas/fileWatchers.js +++ b/src/store/sagas/fileWatchers.js @@ -15,7 +15,7 @@ import * as fileActions from '../actions/files'; import * as taskActions from '../actions/tasks'; import * as uploadActions from '../actions/uploads'; -import { getFileById, getFilesByPath, getSelectedFileIds } from '../reducers/files'; +import { getFileById, getFileIdsByPath, getSelectedFileIds } from '../reducers/files'; import { getCurrentPath } from '../reducers/ui'; /** @@ -82,7 +82,7 @@ function* handleCreateFolder(action) { const { folder } = action.payload; const currPath = yield select(getCurrentPath); - const ids = new Set(yield select(getFilesByPath, currPath)); + const ids = new Set(yield select(getFileIdsByPath, currPath)); if (!ids.has(folder.id)) { const nextFiles = [...ids]; const idx = yield findNextIdx(nextFiles, folder, compareFiles); @@ -108,7 +108,7 @@ function* handleMoveFile(action) { const currPath = yield select(getCurrentPath); const parentPath = routes.parent(file.path); - const ids = yield select(getFilesByPath, currPath); + const ids = yield select(getFileIdsByPath, currPath); const nextFiles = [...ids.filter((id) => id !== file.id)]; if (parentPath === currPath) { const idx = yield findNextIdx(nextFiles, file, compareFiles); @@ -135,7 +135,7 @@ function* handleUpload(action) { } } if (target) { - const ids = new Set(yield select(getFilesByPath, currPath)); + const ids = new Set(yield select(getFileIdsByPath, currPath)); if (!ids.has(target.id)) { const nextFiles = [...ids]; const idx = yield findNextIdx(nextFiles, target, compareFiles); From f04eb4dffba58a6a3d7c027ad30dae22dacafd13 Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Thu, 26 Aug 2021 20:43:41 +0300 Subject: [PATCH 3/4] Reduce rerenders for `FolderPicker` (#18) --- src/components/DeleteDialog.jsx | 3 +- src/components/DeleteImmediatelyDialog.jsx | 3 +- src/components/FileTableView.jsx | 3 +- src/components/FolderPicker.jsx | 28 +++++++---- src/components/FolderPickerItem.jsx | 58 +++++++++++----------- src/components/MoveDialog.jsx | 3 +- src/components/Uploader/UploadList.jsx | 3 +- src/components/ui/VList.jsx | 13 +++-- src/containers/FolderPickerItem.js | 10 ---- src/store/reducers/files.js | 45 ++++++++++++----- 10 files changed, 95 insertions(+), 74 deletions(-) delete mode 100644 src/containers/FolderPickerItem.js diff --git a/src/components/DeleteDialog.jsx b/src/components/DeleteDialog.jsx index 748b86ed..421b2f15 100644 --- a/src/components/DeleteDialog.jsx +++ b/src/components/DeleteDialog.jsx @@ -7,7 +7,7 @@ import { moveToTrashBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { makeGetFilesByIds } from '../store/reducers/files'; +import { getFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -31,7 +31,6 @@ function DeleteDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.movingToTrash)); const fileIds = dialogProps.fileIds ?? []; - const getFilesByIds = makeGetFilesByIds(); const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onDelete = () => { diff --git a/src/components/DeleteImmediatelyDialog.jsx b/src/components/DeleteImmediatelyDialog.jsx index 7a334254..32960485 100644 --- a/src/components/DeleteImmediatelyDialog.jsx +++ b/src/components/DeleteImmediatelyDialog.jsx @@ -7,7 +7,7 @@ import { deleteImmediatelyBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { makeGetFilesByIds } from '../store/reducers/files'; +import { getFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -31,7 +31,6 @@ function DeleteDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.deletingFileImmediately)); const fileIds = dialogProps.fileIds ?? []; - const getFilesByIds = makeGetFilesByIds(); const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onDelete = () => { diff --git a/src/components/FileTableView.jsx b/src/components/FileTableView.jsx index 9866e9f3..cea5caed 100644 --- a/src/components/FileTableView.jsx +++ b/src/components/FileTableView.jsx @@ -63,7 +63,8 @@ function Table({
{(items.length || loading) ? ( (event) => { }; const FolderPicker = ({ excludeIds, path, onPathChange }) => { + const dispatch = useDispatch(); + let items = useSelector((state) => getFolderIdsByPath(state, { path })); const loading = useSelector((state) => getLoading(state, scopes.listingFolder)); React.useEffect(() => { - listFolder(path); - }, [path]); + if (items.length === 0) { + dispatch(listFolder(path)); + } + }, [items.length, path, dispatch]); const idsToExclude = new Set(excludeIds); items = items.filter((id) => !idsToExclude.has(id)); + const data = { + items, + onClick: onPathChange, + }; + return ( <>
@@ -72,13 +81,10 @@ const FolderPicker = ({ excludeIds, path, onPathChange }) => { ( -
- -
- )} + itemRender={FolderPickerItem} /> ) : (
diff --git a/src/components/FolderPickerItem.jsx b/src/components/FolderPickerItem.jsx index 5f456bfa..e0f2a490 100644 --- a/src/components/FolderPickerItem.jsx +++ b/src/components/FolderPickerItem.jsx @@ -1,46 +1,46 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { MediaType } from '../constants'; +import { useSelector } from 'react-redux'; + +import { getFileById } from '../store/reducers/files'; import FileIcon from './FileIcon'; -function FolderPickerItem({ className, item, onClick }) { +function FolderPickerItem({ data, index, style }) { + const itemId = data.items[index]; + const item = useSelector((state) => getFileById(state, itemId)); + const primaryText = (item.hidden) ? 'text-gray-500' : 'text-gray-800'; - const isFolder = item.mediatype === MediaType.FOLDER; - const cursor = (isFolder) ? '' : 'cursor-default'; return ( -
- + +
); } FolderPickerItem.propTypes = { - className: PropTypes.string, - item: PropTypes.shape({ - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - hidden: PropTypes.bool.isRequired, - mediatype: PropTypes.string.isRequired, + data: PropTypes.shape({ + items: PropTypes.arrayOf( + PropTypes.string.isRequired, + ).isRequired, + onClick: PropTypes.func.isRequired, }).isRequired, + index: PropTypes.number.isRequired, }; -FolderPickerItem.defaultProps = { - className: '', -}; - -export default FolderPickerItem; +export default React.memo(FolderPickerItem); diff --git a/src/components/MoveDialog.jsx b/src/components/MoveDialog.jsx index 847e18b9..706a65f2 100644 --- a/src/components/MoveDialog.jsx +++ b/src/components/MoveDialog.jsx @@ -7,7 +7,7 @@ import { moveFileBatch } from '../store/actions/files'; import { scopes } from '../store/actions/loading'; import { closeDialog } from '../store/actions/ui'; -import { makeGetFilesByIds } from '../store/reducers/files'; +import { getFilesByIds } from '../store/reducers/files'; import { getLoading } from '../store/reducers/loading'; import { getFileDialogProps, getFileDialogVisible } from '../store/reducers/ui'; @@ -32,7 +32,6 @@ function MoveDialog({ uid }) { const loading = useSelector((state) => getLoading(state, scopes.movingFile)); const fileIds = dialogProps.fileIds ?? []; - const getFilesByIds = makeGetFilesByIds(); const files = useSelector((state) => getFilesByIds(state, { ids: fileIds }), shallowEqual); const onMove = () => { diff --git a/src/components/Uploader/UploadList.jsx b/src/components/Uploader/UploadList.jsx index 4378f482..71a8b90b 100644 --- a/src/components/Uploader/UploadList.jsx +++ b/src/components/Uploader/UploadList.jsx @@ -11,7 +11,8 @@ function UploadList({ uploads, virtual, itemRender }) { if (virtual) { return ( ); diff --git a/src/components/ui/VList.jsx b/src/components/ui/VList.jsx index 1387b4eb..e7030a83 100644 --- a/src/components/ui/VList.jsx +++ b/src/components/ui/VList.jsx @@ -10,8 +10,9 @@ function VList({ className, heightOffset, initialScrollOffset, + itemCount, + itemData, itemHeight, - items, loading, scrollKey, trackScrolling, @@ -35,8 +36,8 @@ function VList({ ({ - item: getFileById(state, ownProps.item), - }), -)(FolderPickerItem); diff --git a/src/store/reducers/files.js b/src/store/reducers/files.js index 591c1091..e5eab174 100644 --- a/src/store/reducers/files.js +++ b/src/store/reducers/files.js @@ -1,3 +1,4 @@ +import { shallowEqual } from 'react-redux'; import { combineReducers } from 'redux'; import { createSelector } from 'reselect'; @@ -38,10 +39,21 @@ function filesById(state = {}, action) { }; } case types.LIST_FOLDER_SUCCESS: { - return { - ...state, - ...normalize(action.payload.items), - }; + if (action.payload.items.length === 0) { + return state; + } + const items = normalize(action.payload.items); + const nextState = { ...state }; + Object.keys(items).forEach((key) => { + if (nextState[key] == null) { + nextState[key] = items[key]; + } + if (!shallowEqual(nextState[key], items[key])) { + nextState[key] = items[key]; + } + }); + + return nextState; } case types.MOVE_TO_TRASH_SUCCESS: case types.MOVE_FILE_SUCCESS: { @@ -114,10 +126,17 @@ function filesByPath(state = {}, action) { } case types.LIST_FOLDER_SUCCESS: { const { path, items } = action.payload; - return { - ...state, - [path]: items.map((file) => file.id), - }; + if (items.length === 0) { + return state; + } + const ids = items.map((file) => file.id); + if (!shallowEqual(ids, state[path])) { + return { + ...state, + [path]: ids, + }; + } + return state; } case types.UPDATE_FOLDER_BY_PATH: { const { path, ids } = action.payload; @@ -173,7 +192,7 @@ export default combineReducers({ const FILES_EMPTY = []; export const getFileById = (state, id) => state.files.byId[id]; -export const getFileIdsByPath = (state, path) => state.files.byPath[path] || FILES_EMPTY; +export const getFileIdsByPath = (state, path) => state.files.byPath[path] ?? FILES_EMPTY; export const getFilesCountByPath = (state, path) => getFileIdsByPath(state, path).length; export const getIsFileSelected = (state, id) => state.files.selectedIds.has(id); export const getSelectedFileIds = (state) => [...state.files.selectedIds]; @@ -199,18 +218,20 @@ export const getFolderIdsByPath = createSelector( getPathProp, ], (byId, byPath, path) => ( - (byPath[path] || FILES_EMPTY) + (byPath[path] ?? FILES_EMPTY) .map((id) => byId[id]) .filter((item) => item.mediatype === MediaType.FOLDER) .map((item) => item.id) ), ); -export const makeGetFilesByIds = () => ( +const getIdsProps = createPropsSelector((props) => props.ids); + +export const getFilesByIds = ( createSelector( [ (state) => state.files.byId, - (_state, props) => props.ids, + getIdsProps, ], (byId, ids) => ( ids.map((id) => byId[id]) From cd65ab2394c0894c8831b3b402d75764f1c1f2ba Mon Sep 17 00:00:00 2001 From: Aleksei Maslakov Date: Thu, 26 Aug 2021 23:24:57 +0300 Subject: [PATCH 4/4] Reduce re-renders for `SidePreview` (#18) --- src/store/reducers/files.js | 16 +++++++++++++--- src/store/sagas/fileWatchers.js | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/store/reducers/files.js b/src/store/reducers/files.js index e5eab174..7a7a5061 100644 --- a/src/store/reducers/files.js +++ b/src/store/reducers/files.js @@ -195,9 +195,19 @@ export const getFileById = (state, id) => state.files.byId[id]; export const getFileIdsByPath = (state, path) => state.files.byPath[path] ?? FILES_EMPTY; export const getFilesCountByPath = (state, path) => getFileIdsByPath(state, path).length; export const getIsFileSelected = (state, id) => state.files.selectedIds.has(id); -export const getSelectedFileIds = (state) => [...state.files.selectedIds]; -export const getSelectedFiles = (state) => ( - getSelectedFileIds(state).map((id) => getFileById(state, id)) +export const getSelectedFileIds = (state) => state.files.selectedIds; +export const getSelectedFiles = createSelector( + [ + (state) => state.files.byId, + getSelectedFileIds, + ], + (byId, fileIds) => { + const files = []; + fileIds.forEach((id) => { + files.push(byId[id]); + }); + return files; + }, ); export const getHasSelectedFiles = (state) => state.files.selectedIds.size !== 0; export const getCountSelectedFiles = (state) => state.files.selectedIds.size; diff --git a/src/store/sagas/fileWatchers.js b/src/store/sagas/fileWatchers.js index e9b14aed..70b31a84 100644 --- a/src/store/sagas/fileWatchers.js +++ b/src/store/sagas/fileWatchers.js @@ -97,7 +97,7 @@ function* handleListFolder({ payload }) { if (path === currentPath) { const fileIds = new Set(items.map((item) => item.id)); - const selectedFiles = new Set(yield select(getSelectedFileIds)); + const selectedFiles = yield select(getSelectedFileIds); const fileIdsToDeselect = difference(selectedFiles, fileIds); yield put(fileActions.bulkDeselectFiles(fileIdsToDeselect)); }