From 453796fd6c3ba93a3d2980a446cd1cbf4f45ad02 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 12 Jan 2024 14:49:53 +0500 Subject: [PATCH 1/2] perf: add memo, useMemo and useCallback to bind the references and avoid unnecessary re-renders --- src/components/LHNOptionsList/LHNOptionsList.js | 4 ++-- src/components/OptionsList/BaseOptionsList.js | 8 +++++--- src/components/OptionsList/index.js | 4 ++-- src/components/OptionsList/index.native.js | 6 +++--- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 14 +++++++------- src/pages/home/sidebar/SidebarLinks.js | 7 +++++-- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 71b14b6fadcd..d1bf02b08191 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,7 +1,7 @@ import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {memo, useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -190,4 +190,4 @@ export default compose( key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, }), -)(LHNOptionsList); +)(memo(LHNOptionsList)); diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index bd3695eb7aa9..75025641468e 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {forwardRef, memo, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import OptionRow from '@components/OptionRow'; @@ -51,7 +51,7 @@ function BaseOptionsList({ shouldHaveOptionSeparator, showTitleTooltip, optionHoveredStyle, - contentContainerStyles, + contentContainerStyles: contentContainerStylesProp, sectionHeaderStyle, showScrollIndicator, listContainerStyles: listContainerStylesProp, @@ -72,13 +72,15 @@ function BaseOptionsList({ nestedScrollEnabled, bounces, renderFooterContent, + safeAreaPaddingBottomStyle, }) { const styles = useThemeStyles(); const flattenedData = useRef(); const previousSections = usePrevious(sections); const didLayout = useRef(false); - const listContainerStyles = listContainerStylesProp || [styles.flex1]; + const listContainerStyles = useMemo(() => listContainerStylesProp || [styles.flex1], [listContainerStylesProp, styles.flex1]); + const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, ...contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]); /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. diff --git a/src/components/OptionsList/index.js b/src/components/OptionsList/index.js index 36b8e7fccf12..6046a6124ccc 100644 --- a/src/components/OptionsList/index.js +++ b/src/components/OptionsList/index.js @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useCallback, useEffect, useRef} from 'react'; import {Keyboard} from 'react-native'; import _ from 'underscore'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -64,4 +64,4 @@ const OptionsListWithRef = forwardRef((props, ref) => ( OptionsListWithRef.displayName = 'OptionsListWithRef'; -export default withWindowDimensions(OptionsListWithRef); +export default withWindowDimensions(memo(OptionsListWithRef)); diff --git a/src/components/OptionsList/index.native.js b/src/components/OptionsList/index.native.js index ab2db4f20967..8a70e1e060b1 100644 --- a/src/components/OptionsList/index.native.js +++ b/src/components/OptionsList/index.native.js @@ -1,4 +1,4 @@ -import React, {forwardRef} from 'react'; +import React, {forwardRef, memo} from 'react'; import {Keyboard} from 'react-native'; import BaseOptionsList from './BaseOptionsList'; import {defaultProps, propTypes} from './optionsListPropTypes'; @@ -8,7 +8,7 @@ const OptionsList = forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onScrollBeginDrag={() => Keyboard.dismiss()} + onScrollBeginDrag={Keyboard.dismiss} /> )); @@ -16,4 +16,4 @@ OptionsList.propTypes = propTypes; OptionsList.defaultProps = defaultProps; OptionsList.displayName = 'OptionsList'; -export default OptionsList; +export default memo(OptionsList); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dba8ef2e11d0..fd2e5e7d8f57 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,7 +1,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; @@ -513,4 +513,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(memo(ReportActionsList)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 2758437a3962..8c25a65d3d3e 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -174,25 +174,25 @@ function ReportActionsView(props) { } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. if (props.network.isOffline || props.isLoadingOlderReportActions) { return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }; + }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -229,7 +229,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { + const recordTimeToMeasureItemLayout = useCallback(() => { if (didLayout.current) { return; } @@ -244,7 +244,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }; + }, [hasCachedActions]); // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ffcba2048d18..955b813f007e 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import _ from 'underscore'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; @@ -150,6 +150,9 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; + // eslint-disable-next-line react-hooks/exhaustive-deps + const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); + return ( Date: Fri, 12 Jan 2024 14:51:43 +0500 Subject: [PATCH 2/2] perf: execute getSearchOptions when transition is finished --- src/pages/SearchPage.js | 64 ++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 061f43e73de8..6086c358b1b3 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -42,6 +43,18 @@ const defaultProps = { isSearchingForReports: false, }; +function isSectionsEmpty(sections) { + if (!sections.length) { + return true; + } + + if (!sections[0].data.length) { + return true; + } + + return _.isEmpty(sections[0].data[0]); +} + function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const [searchValue, setSearchValue] = useState(''); const [searchOptions, setSearchOptions] = useState({ @@ -54,21 +67,45 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { const {translate} = useLocalize(); const themeStyles = useThemeStyles(); const isMounted = useRef(false); + const interactionTask = useRef(null); const updateOptions = useCallback(() => { - const { - recentReports: localRecentReports, - personalDetails: localPersonalDetails, - userToInvite: localUserToInvite, - } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas); - - setSearchOptions({ - recentReports: localRecentReports, - personalDetails: localPersonalDetails, - userToInvite: localUserToInvite, + if (interactionTask.current) { + interactionTask.current.cancel(); + } + + /** + * Execute the callback after all interactions are done, which means + * after all animations have finished. + */ + interactionTask.current = InteractionManager.runAfterInteractions(() => { + const { + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, + } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas); + + setSearchOptions({ + recentReports: localRecentReports, + personalDetails: localPersonalDetails, + userToInvite: localUserToInvite, + }); }); }, [reports, personalDetails, searchValue, betas]); + /** + * Cancel the interaction task when the component unmounts + */ + useEffect( + () => () => { + if (!interactionTask.current) { + return; + } + interactionTask.current.cancel(); + }, + [], + ); + useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); Performance.markStart(CONST.TIMING.SEARCH_RENDER); @@ -160,6 +197,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { searchValue, ); + const sections = getSections(); return (