From ac759f24139e235981b70ebd0f3ecd33d4fbc922 Mon Sep 17 00:00:00 2001 From: Github Date: Wed, 28 May 2025 17:30:40 +0200 Subject: [PATCH 1/3] perf: move screen focus to LHNOptionsList to avoid re-renders --- .../LHNOptionsList/LHNOptionsList.tsx | 7 +++- .../LHNOptionsList/OptionRowLHN.tsx | 32 +++++++------------ .../LHNOptionsList/OptionRowLHNData.tsx | 6 ++-- src/components/LHNOptionsList/types.ts | 12 +++++-- src/hooks/useIsScreenFocus.ts | 17 ++++++++++ 5 files changed, 46 insertions(+), 28 deletions(-) create mode 100644 src/hooks/useIsScreenFocus.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 1838ae63dd3e..dfc10b690e0e 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import TextBlock from '@components/TextBlock'; +import useIsScreenFocused from '@hooks/useIsScreenFocus'; import useLHNEstimatedListSize from '@hooks/useLHNEstimatedListSize'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -57,6 +58,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const styles = useThemeStyles(); const {translate, preferredLocale} = useLocalize(); const estimatedListSize = useLHNEstimatedListSize(); + const isScreenFocused = useIsScreenFocused(); + const shouldShowEmptyLHN = data.length === 0; const estimatedItemSize = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight; const platform = getPlatform(); @@ -224,7 +227,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio lastReportActionTransaction={lastReportActionTransaction} receiptTransactions={transactions} viewMode={optionMode} - isFocused={!shouldDisableFocusOptions} + isOptionFocused={!shouldDisableFocusOptions} lastMessageTextFromReport={lastMessageTextFromReport} onSelectRow={onSelectRow} preferredLocale={preferredLocale} @@ -232,6 +235,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio transactionViolations={transactionViolations} onLayout={onLayoutItem} shouldShowRBRorGBRTooltip={shouldShowRBRorGBRTooltip} + isScreenFocused={isScreenFocused} /> ); }, @@ -252,6 +256,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio onLayoutItem, isOffline, firstReportIDWithGBRorRBR, + isScreenFocused, ], ); diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index da12c0ce5eb2..203567eff30a 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,4 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -51,7 +50,7 @@ import type {OptionRowLHNProps} from './types'; function OptionRowLHN({ reportID, - isFocused = false, + isOptionFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', @@ -59,12 +58,12 @@ function OptionRowLHN({ onLayout = () => {}, hasDraftComment, shouldShowRBRorGBRTooltip, + isScreenFocused, }: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); const StyleUtils = useStyleUtils(); - const [isScreenFocused, setIsScreenFocused] = useState(false); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID}`, {canBeMissing: true}); @@ -105,15 +104,6 @@ function OptionRowLHN({ const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); - useFocusEffect( - useCallback(() => { - setIsScreenFocused(true); - return () => { - setIsScreenFocused(false); - }; - }, []), - ); - const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -121,7 +111,7 @@ function OptionRowLHN({ : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); - if (!optionItem && !isFocused) { + if (!optionItem && !isOptionFocused) { // rendering null as a render item causes the FlashList to render all // its children and consume significant memory on the first render. We can avoid this by // rendering a placeholder view instead. This behaviour is only observed when we @@ -140,7 +130,7 @@ function OptionRowLHN({ } const brickRoadIndicator = optionItem.brickRoadIndicator; - const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; const alternateTextStyle = isInFocusMode @@ -188,7 +178,7 @@ function OptionRowLHN({ const statusContent = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText; const isStatusVisible = !!emojiCode && isOneOnOneChat(!isEmptyObject(report) ? report : undefined); - const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; + const subscriptAvatarBorderColor = isOptionFocused ? focusedBackgroundColor : theme.sidebar; const firstIcon = optionItem.icons?.at(0); const onOptionPress = (event: GestureResponderEvent | KeyboardEvent | undefined) => { @@ -256,8 +246,8 @@ function OptionRowLHN({ styles.sidebarLink, styles.sidebarLinkInnerLHN, StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + isOptionFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isOptionFocused ? styles.sidebarLinkHover : null, ]} role={CONST.ROLE.BUTTON} accessibilityLabel={`${translate('accessibilityHints.navigatesToChat')} ${optionItem.text}. ${optionItem.isUnread ? `${translate('common.unread')}.` : ''} ${ @@ -272,7 +262,7 @@ function OptionRowLHN({ firstIcon && (optionItem.shouldShowSubscript ? ( diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 94f691529fdb..45f376f8a9d0 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -14,7 +14,7 @@ import type {OptionRowLHNDataProps} from './types'; * re-render if the data really changed. */ function OptionRowLHNData({ - isFocused = false, + isOptionFocused = false, fullReport, reportAttributes, oneTransactionThreadReport, @@ -35,7 +35,7 @@ function OptionRowLHNData({ }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; const currentReportIDValue = useCurrentReportID(); - const isReportFocused = isFocused && currentReportIDValue?.currentReportID === reportID; + const isReportFocused = isOptionFocused && currentReportIDValue?.currentReportID === reportID; const optionItemRef = useRef(undefined); const optionItem = useMemo(() => { @@ -90,7 +90,7 @@ function OptionRowLHNData({ ); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index ef5a8d7e9d02..8a79f93e6491 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -36,8 +36,8 @@ type CustomLHNOptionsListProps = { type LHNOptionsListProps = CustomLHNOptionsListProps; type OptionRowLHNDataProps = { - /** Whether row should be focused */ - isFocused?: boolean; + /** Whether option should be focused */ + isOptionFocused?: boolean; /** List of users' personal details */ personalDetails?: PersonalDetailsList; @@ -109,6 +109,9 @@ type OptionRowLHNDataProps = { /** Whether to show the educational tooltip for the GBR or RBR */ shouldShowRBRorGBRTooltip: boolean; + + /** Whether the screen is focused */ + isScreenFocused: boolean; }; type OptionRowLHNProps = { @@ -116,7 +119,7 @@ type OptionRowLHNProps = { reportID: string; /** Whether this option is currently in focus so we can modify its style */ - isFocused?: boolean; + isOptionFocused?: boolean; /** A function that is called when an option is selected. Selected option is passed as a param */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -137,6 +140,9 @@ type OptionRowLHNProps = { /** Whether to show the educational tooltip on the GBR or RBR */ shouldShowRBRorGBRTooltip: boolean; + + /** Whether the screen is focused */ + isScreenFocused: boolean; }; type RenderItemProps = {item: Report}; diff --git a/src/hooks/useIsScreenFocus.ts b/src/hooks/useIsScreenFocus.ts new file mode 100644 index 000000000000..98df6178853d --- /dev/null +++ b/src/hooks/useIsScreenFocus.ts @@ -0,0 +1,17 @@ +import {useFocusEffect} from '@react-navigation/native'; +import {useCallback, useState} from 'react'; + +export default function useIsScreenFocusedRef() { + const [isScreenFocused, setIsScreenFocused] = useState(false); + + useFocusEffect( + useCallback(() => { + setIsScreenFocused(true); + return () => { + setIsScreenFocused(false); + }; + }, []), + ); + + return isScreenFocused; +} From f41dcff608ccde1a1ca422e86f388909d6230d53 Mon Sep 17 00:00:00 2001 From: Github Date: Thu, 29 May 2025 10:19:22 +0200 Subject: [PATCH 2/3] fix typo --- src/hooks/useIsScreenFocus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useIsScreenFocus.ts b/src/hooks/useIsScreenFocus.ts index 98df6178853d..bbeef1373217 100644 --- a/src/hooks/useIsScreenFocus.ts +++ b/src/hooks/useIsScreenFocus.ts @@ -1,7 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import {useCallback, useState} from 'react'; -export default function useIsScreenFocusedRef() { +export default function useIsScreenFocused() { const [isScreenFocused, setIsScreenFocused] = useState(false); useFocusEffect( From 2c2fd146d9b093d138ba6a1abd2468338214b274 Mon Sep 17 00:00:00 2001 From: Github Date: Thu, 29 May 2025 12:13:32 +0200 Subject: [PATCH 3/3] add useIsFocused instead of custom hook --- .../LHNOptionsList/LHNOptionsList.tsx | 5 ++--- src/hooks/useIsScreenFocus.ts | 17 ----------------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 src/hooks/useIsScreenFocus.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index dfc10b690e0e..92df46c78588 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,4 +1,4 @@ -import {useRoute} from '@react-navigation/native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; import type {FlashListProps} from '@shopify/flash-list'; import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; @@ -11,7 +11,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import TextBlock from '@components/TextBlock'; -import useIsScreenFocused from '@hooks/useIsScreenFocus'; import useLHNEstimatedListSize from '@hooks/useLHNEstimatedListSize'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -58,7 +57,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const styles = useThemeStyles(); const {translate, preferredLocale} = useLocalize(); const estimatedListSize = useLHNEstimatedListSize(); - const isScreenFocused = useIsScreenFocused(); + const isScreenFocused = useIsFocused(); const shouldShowEmptyLHN = data.length === 0; const estimatedItemSize = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight; diff --git a/src/hooks/useIsScreenFocus.ts b/src/hooks/useIsScreenFocus.ts deleted file mode 100644 index bbeef1373217..000000000000 --- a/src/hooks/useIsScreenFocus.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import {useCallback, useState} from 'react'; - -export default function useIsScreenFocused() { - const [isScreenFocused, setIsScreenFocused] = useState(false); - - useFocusEffect( - useCallback(() => { - setIsScreenFocused(true); - return () => { - setIsScreenFocused(false); - }; - }, []), - ); - - return isScreenFocused; -}