refactor(reporting): optimize project reports components with memoization
- Refactored ProjectsReports, ProjectsReportsFilters, and ProjectsReportsTable components to utilize React.memo, useCallback, and useMemo for improved performance and reduced unnecessary re-renders. - Memoized various handlers and configurations to enhance rendering efficiency and maintain responsiveness. - Updated component exports to use memoization, ensuring optimal performance during re-renders.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import { useMemo, useCallback, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ProjectStatusFilterDropdown from './project-status-filter-dropdown';
|
import ProjectStatusFilterDropdown from './project-status-filter-dropdown';
|
||||||
import ProjectHealthFilterDropdown from './project-health-filter-dropdown';
|
import ProjectHealthFilterDropdown from './project-health-filter-dropdown';
|
||||||
@@ -15,26 +16,39 @@ const ProjectsReportsFilters = () => {
|
|||||||
const { t } = useTranslation('reporting-projects-filters');
|
const { t } = useTranslation('reporting-projects-filters');
|
||||||
const { searchQuery } = useAppSelector(state => state.projectReportsReducer);
|
const { searchQuery } = useAppSelector(state => state.projectReportsReducer);
|
||||||
|
|
||||||
|
// Memoize the search query handler to prevent recreation on every render
|
||||||
|
const handleSearchQueryChange = useCallback((text: string) => {
|
||||||
|
dispatch(setSearchQuery(text));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// Memoize the filter dropdowns container to prevent recreation on every render
|
||||||
|
const filterDropdowns = useMemo(() => (
|
||||||
|
<Flex gap={8} wrap={'wrap'}>
|
||||||
|
<ProjectStatusFilterDropdown />
|
||||||
|
<ProjectHealthFilterDropdown />
|
||||||
|
<ProjectCategoriesFilterDropdown />
|
||||||
|
<ProjectManagersFilterDropdown />
|
||||||
|
</Flex>
|
||||||
|
), []);
|
||||||
|
|
||||||
|
// Memoize the right side controls to prevent recreation on every render
|
||||||
|
const rightControls = useMemo(() => (
|
||||||
|
<Flex gap={12}>
|
||||||
|
<ProjectTableShowFieldsDropdown />
|
||||||
|
<CustomSearchbar
|
||||||
|
placeholderText={t('searchByNamePlaceholder')}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
setSearchQuery={handleSearchQueryChange}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
), [t, searchQuery, handleSearchQueryChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={8} align="center" justify="space-between">
|
<Flex gap={8} align="center" justify="space-between">
|
||||||
<Flex gap={8} wrap={'wrap'}>
|
{filterDropdowns}
|
||||||
<ProjectStatusFilterDropdown />
|
{rightControls}
|
||||||
<ProjectHealthFilterDropdown />
|
|
||||||
<ProjectCategoriesFilterDropdown />
|
|
||||||
<ProjectManagersFilterDropdown />
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex gap={12}>
|
|
||||||
<ProjectTableShowFieldsDropdown />
|
|
||||||
|
|
||||||
<CustomSearchbar
|
|
||||||
placeholderText={t('searchByNamePlaceholder')}
|
|
||||||
searchQuery={searchQuery}
|
|
||||||
setSearchQuery={text => dispatch(setSearchQuery(text))}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectsReportsFilters;
|
export default memo(ProjectsReportsFilters);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo, useCallback, memo } from 'react';
|
||||||
import { Button, ConfigProvider, Flex, PaginationProps, Table, TableColumnsType } from 'antd';
|
import { Button, ConfigProvider, Flex, PaginationProps, Table, TableColumnsType } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
@@ -63,10 +63,11 @@ const ProjectsReportsTable = () => {
|
|||||||
|
|
||||||
const columnsVisibility = useAppSelector(state => state.projectReportsTableColumnsReducer);
|
const columnsVisibility = useAppSelector(state => state.projectReportsTableColumnsReducer);
|
||||||
|
|
||||||
const handleDrawerOpen = (record: IRPTProject) => {
|
// Memoize the drawer open handler to prevent recreation on every render
|
||||||
|
const handleDrawerOpen = useCallback((record: IRPTProject) => {
|
||||||
setSelectedProject(record);
|
setSelectedProject(record);
|
||||||
dispatch(toggleProjectReportsDrawer());
|
dispatch(toggleProjectReportsDrawer());
|
||||||
};
|
}, [dispatch]);
|
||||||
|
|
||||||
const columns: TableColumnsType<IRPTProject> = useMemo(
|
const columns: TableColumnsType<IRPTProject> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -231,7 +232,7 @@ const ProjectsReportsTable = () => {
|
|||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, order]
|
[t, order, handleDrawerOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter columns based on the `hidden` state from Redux
|
// filter columns based on the `hidden` state from Redux
|
||||||
@@ -240,12 +241,13 @@ const ProjectsReportsTable = () => {
|
|||||||
[columns, columnsVisibility]
|
[columns, columnsVisibility]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTableChange = (pagination: PaginationProps, filters: any, sorter: any) => {
|
// Memoize the table change handler to prevent recreation on every render
|
||||||
|
const handleTableChange = useCallback((pagination: PaginationProps, filters: any, sorter: any) => {
|
||||||
if (sorter.order) dispatch(setOrder(sorter.order));
|
if (sorter.order) dispatch(setOrder(sorter.order));
|
||||||
if (sorter.field) dispatch(setField(sorter.field));
|
if (sorter.field) dispatch(setField(sorter.field));
|
||||||
dispatch(setIndex(pagination.current));
|
dispatch(setIndex(pagination.current));
|
||||||
dispatch(setPageSize(pagination.pageSize));
|
dispatch(setPageSize(pagination.pageSize));
|
||||||
};
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading) dispatch(fetchProjectData());
|
if (!isLoading) dispatch(fetchProjectData());
|
||||||
@@ -268,7 +270,7 @@ const ProjectsReportsTable = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
dispatch(resetProjectReports());
|
dispatch(resetProjectReports());
|
||||||
};
|
};
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
const tableRowProps = useMemo(
|
const tableRowProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -292,27 +294,39 @@ const ProjectsReportsTable = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Memoize pagination configuration to prevent recreation on every render
|
||||||
|
const paginationConfig = useMemo(() => ({
|
||||||
|
showSizeChanger: true,
|
||||||
|
defaultPageSize: 10,
|
||||||
|
total: total,
|
||||||
|
current: index,
|
||||||
|
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
||||||
|
}), [total, index]);
|
||||||
|
|
||||||
|
// Memoize scroll configuration to prevent recreation on every render
|
||||||
|
const scrollConfig = useMemo(() => ({ x: 'max-content' }), []);
|
||||||
|
|
||||||
|
// Memoize row key function to prevent recreation on every render
|
||||||
|
const getRowKey = useCallback((record: IRPTProject) => record.id, []);
|
||||||
|
|
||||||
|
// Memoize onRow function to prevent recreation on every render
|
||||||
|
const getRowProps = useCallback(() => tableRowProps, [tableRowProps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider {...tableConfig}>
|
<ConfigProvider {...tableConfig}>
|
||||||
<Table
|
<Table
|
||||||
columns={visibleColumns}
|
columns={visibleColumns}
|
||||||
dataSource={projectList}
|
dataSource={projectList}
|
||||||
pagination={{
|
pagination={paginationConfig}
|
||||||
showSizeChanger: true,
|
scroll={scrollConfig}
|
||||||
defaultPageSize: 10,
|
|
||||||
total: total,
|
|
||||||
current: index,
|
|
||||||
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
|
||||||
}}
|
|
||||||
scroll={{ x: 'max-content' }}
|
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
rowKey={record => record.id}
|
rowKey={getRowKey}
|
||||||
onRow={() => tableRowProps}
|
onRow={getRowProps}
|
||||||
/>
|
/>
|
||||||
{createPortal(<ProjectReportsDrawer selectedProject={selectedProject} />, document.body)}
|
{createPortal(<ProjectReportsDrawer selectedProject={selectedProject} />, document.body)}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectsReportsTable;
|
export default memo(ProjectsReportsTable);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Card, Checkbox, Dropdown, Flex, Space, Typography } from 'antd';
|
import { Button, Card, Checkbox, Dropdown, Flex, Space, Typography } from 'antd';
|
||||||
|
import { useMemo, useCallback, memo } from 'react';
|
||||||
import CustomPageHeader from '@/pages/reporting/page-header/custom-page-header';
|
import CustomPageHeader from '@/pages/reporting/page-header/custom-page-header';
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
import ProjectReportsTable from './projects-reports-table/projects-reports-table';
|
import ProjectReportsTable from './projects-reports-table/projects-reports-table';
|
||||||
@@ -20,40 +21,60 @@ const ProjectsReports = () => {
|
|||||||
|
|
||||||
const { total, archived } = useAppSelector(state => state.projectReportsReducer);
|
const { total, archived } = useAppSelector(state => state.projectReportsReducer);
|
||||||
|
|
||||||
const handleExcelExport = () => {
|
// Memoize the title to prevent recalculation on every render
|
||||||
|
const pageTitle = useMemo(() => {
|
||||||
|
return `${total === 1 ? `${total} ${t('projectCount')}` : `${total} ${t('projectCountPlural')}`} `;
|
||||||
|
}, [total, t]);
|
||||||
|
|
||||||
|
// Memoize the Excel export handler to prevent recreation on every render
|
||||||
|
const handleExcelExport = useCallback(() => {
|
||||||
if (currentSession?.team_name) {
|
if (currentSession?.team_name) {
|
||||||
reportingExportApiService.exportProjects(currentSession.team_name);
|
reportingExportApiService.exportProjects(currentSession.team_name);
|
||||||
}
|
}
|
||||||
};
|
}, [currentSession?.team_name]);
|
||||||
|
|
||||||
|
// Memoize the archived checkbox handler to prevent recreation on every render
|
||||||
|
const handleArchivedChange = useCallback(() => {
|
||||||
|
dispatch(setArchived(!archived));
|
||||||
|
}, [dispatch, archived]);
|
||||||
|
|
||||||
|
// Memoize the dropdown menu items to prevent recreation on every render
|
||||||
|
const dropdownMenuItems = useMemo(() => [
|
||||||
|
{ key: '1', label: t('excelButton'), onClick: handleExcelExport }
|
||||||
|
], [t, handleExcelExport]);
|
||||||
|
|
||||||
|
// Memoize the header children to prevent recreation on every render
|
||||||
|
const headerChildren = useMemo(() => (
|
||||||
|
<Space>
|
||||||
|
<Button>
|
||||||
|
<Checkbox checked={archived} onChange={handleArchivedChange}>
|
||||||
|
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
|
||||||
|
</Checkbox>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown menu={{ items: dropdownMenuItems }}>
|
||||||
|
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
|
||||||
|
{t('exportButton')}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
), [archived, handleArchivedChange, t, dropdownMenuItems]);
|
||||||
|
|
||||||
|
// Memoize the card title to prevent recreation on every render
|
||||||
|
const cardTitle = useMemo(() => <ProjectsReportsFilters />, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<CustomPageHeader
|
<CustomPageHeader
|
||||||
title={`${total === 1 ? `${total} ${t('projectCount')}` : `${total} ${t('projectCountPlural')}`} `}
|
title={pageTitle}
|
||||||
children={
|
children={headerChildren}
|
||||||
<Space>
|
|
||||||
<Button>
|
|
||||||
<Checkbox checked={archived} onChange={() => dispatch(setArchived(!archived))}>
|
|
||||||
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
|
|
||||||
</Checkbox>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
menu={{ items: [{ key: '1', label: t('excelButton'), onClick: handleExcelExport }] }}
|
|
||||||
>
|
|
||||||
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
|
|
||||||
{t('exportButton')}
|
|
||||||
</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card title={<ProjectsReportsFilters />}>
|
<Card title={cardTitle}>
|
||||||
<ProjectReportsTable />
|
<ProjectReportsTable />
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectsReports;
|
export default memo(ProjectsReports);
|
||||||
|
|||||||
Reference in New Issue
Block a user