From 6921051449b209820347247511a943d8b4624ffb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 15 Feb 2024 19:19:58 +0500 Subject: [PATCH] perf: improve renderItem and use FlatList --- .../LHNOptionsList/LHNOptionsList.tsx | 114 +++--------------- .../LHNOptionsList/OptionRowLHNData.tsx | 51 +------- src/components/LHNOptionsList/types.ts | 3 +- src/components/withCurrentReportID.tsx | 10 +- src/hooks/useOrderedReportIDs.tsx | 38 +++++- src/libs/SidebarUtils.ts | 40 +++++- src/pages/home/sidebar/SidebarLinks.js | 4 +- src/pages/home/sidebar/SidebarLinksData.js | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 4 +- 9 files changed, 111 insertions(+), 155 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index e0a869f5222a..c70e355c46c5 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,41 +1,27 @@ -import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; -import {StyleSheet, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import withCurrentReportID from '@components/withCurrentReportID'; -import usePermissions from '@hooks/usePermissions'; -import {useReports} from '@hooks/useReports'; +import React, {useCallback, memo} from 'react'; +import {FlatList, StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, RenderItemProps} from './types'; +import { OrderedReports } from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import variables from '@styles/variables'; +import { FlashList } from '@shopify/flash-list'; -const keyExtractor = (item: string) => `report_${item}`; +const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; function LHNOptionsList({ style, contentContainerStyles, data, onSelectRow, - optionMode, shouldDisableFocusOptions = false, - reportActions = {}, - policy = {}, - preferredLocale = CONST.LOCALES.DEFAULT, - personalDetails = {}, - transactions = {}, currentReportID = '', - draftComments = {}, - transactionViolations = {}, + optionMode, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { - const reports = useReports(); const styles = useThemeStyles(); - const {canUseViolations} = usePermissions(); // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -53,69 +39,29 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item: reportID}: RenderItemProps): ReactElement => { - const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; - const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); - const lastReportAction = sortedReportActions[0]; - - // Get the transaction for the last report action - let lastReportActionTransactionID = ''; - - if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; - } - const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + ({item}: RenderItemProps): ReactElement => { return ( ); }, [ currentReportID, - draftComments, onSelectRow, - optionMode, - personalDetails, - policy, - preferredLocale, - reportActions, - reports, - shouldDisableFocusOptions, - transactions, - transactionViolations, - canUseViolations, onLayoutItem, ], ); return ( - @@ -133,30 +79,6 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default withCurrentReportID( - withOnyx({ - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - })(memo(LHNOptionsList)), -); +export default memo(LHNOptionsList); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index a18d5a8ec1ec..622d7b9d9123 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -16,61 +16,12 @@ import type {OptionRowLHNDataProps} from './types'; */ function OptionRowLHNData({ isFocused = false, - fullReport, - reportActions, - personalDetails = {}, - preferredLocale = CONST.LOCALES.DEFAULT, comment, - policy, - receiptTransactions, - parentReportAction, - transaction, - lastReportActionTransaction = {}, - transactionViolations, - canUseViolations, + optionItem, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const optionItemRef = useRef(); - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); - - const optionItem = useMemo(() => { - // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData({ - report: fullReport, - reportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy, - parentReportAction, - hasViolations: !!hasViolations, - }); - if (deepEqual(item, optionItemRef.current)) { - return optionItemRef.current; - } - - optionItemRef.current = item; - - return item; - // Listen parentReportAction to update title of thread report when parentReportAction changed - // Listen to transaction to update title of transaction report when transaction changed - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - fullReport, - lastReportActionTransaction, - reportActions, - personalDetails, - preferredLocale, - policy, - parentReportAction, - transaction, - transactionViolations, - canUseViolations, - receiptTransactions, - ]); - useEffect(() => { if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index c58770c8383f..4c79536571bf 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -8,6 +8,7 @@ import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import { OrderedReports } from '@libs/SidebarUtils'; type OptionMode = ValueOf; @@ -134,6 +135,6 @@ type OptionRowLHNProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: string}; +type RenderItemProps = {item: OrderedReports}; export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..cc49c44e0e77 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,15 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** + * This is to make sure we don't set the undefined as reportID when + * switching between chat list and settings->workspaces tab. + * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + */ + if (reportID) { + setCurrentReportID(reportID); + } }, [setCurrentReportID], ); diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 3217830054de..01dab5a3231a 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -1,6 +1,7 @@ import React, {createContext, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {usePersonalDetails} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -10,6 +11,7 @@ import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, Transacti import type PriorityMode from '@src/types/onyx/PriorityMode'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; +import usePermissions from './usePermissions'; import {useReports} from './useReports'; type OnyxProps = { @@ -31,6 +33,8 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP const chatReports = useReports(); const currentReportIDValue = useCurrentReportID(); const {activeWorkspaceID} = useActiveWorkspace(); + const personalDetails = usePersonalDetails(); + const {canUseViolations} = usePermissions(); const policyMemberAccountIDs = useMemo( () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), @@ -49,8 +53,25 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ), - [chatReports, props.betas, props.policies, props.priorityMode, props.allReportActions, props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + [ + chatReports, + props.betas, + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, + ], ); // We need to make sure the current report is in the list of reports, but we do not want @@ -70,6 +91,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ); } return optionListItems; @@ -84,6 +109,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.policies, props.priorityMode, props.transactionViolations, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ]); return {props.children}; @@ -136,6 +165,13 @@ const OrderedReportIDsContextProvider = withOnyx, currentPolicyID = '', policyMemberAccountIDs: number[] = [], -): string[] { + personalDetails: OnyxEntry, + preferredLocale: DeepValueOf, + canUseViolations: boolean, + draftComments: OnyxCollection, +): OrderedReports[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports ?? {}); @@ -190,7 +200,33 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => { + const itemFullReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`] ?? null; + const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? null; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report?.reportID}`] ?? ''; + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); + + const item = getOptionData({ + report: itemFullReport, + reportActions: itemReportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy: itemPolicy, + parentReportAction: itemParentReportAction, + hasViolations: !!hasViolations, + }); + + return { + reportID: report.reportID, + optionItem: item, + comment: itemComment, + } + }); + return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index e78e656e0b7e..4c05c3d44ae8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -179,5 +179,5 @@ export default withOnyx({ activePolicy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, -})(SidebarLinks); +})(memo(SidebarLinks)); export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 42681fea451b..87006d3debe2 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -46,7 +46,7 @@ const defaultProps = { }, }; -function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { +function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d778648ea998..968148a8becb 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -107,6 +107,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } +const stickyHeaderIndices = [0]; + function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { const reports = useReports(); const theme = useTheme(); @@ -375,7 +377,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: data={workspaces} renderItem={getMenuItem} ListHeaderComponent={listHeaderComponent} - stickyHeaderIndices={[0]} + stickyHeaderIndices={stickyHeaderIndices} />