-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[No QA] Show skeleton in BaseSidebarScreen to avoid mounting heavy SidebarLinks during initial load #84153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[No QA] Show skeleton in BaseSidebarScreen to avoid mounting heavy SidebarLinks during initial load #84153
Changes from all commits
8326a55
4b1a69d
7506083
32977a6
80bb9d1
1e2f871
c4f0cdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,49 @@ | ||
| import React from 'react'; | ||
| import {View} from 'react-native'; | ||
| import Onyx from 'react-native-onyx'; | ||
| import NavigationTabBar from '@components/Navigation/NavigationTabBar'; | ||
| import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; | ||
| import TopBar from '@components/Navigation/TopBar'; | ||
| import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; | ||
| import ScreenWrapper from '@components/ScreenWrapper'; | ||
| import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import {isMobile} from '@libs/Browser'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import SidebarLinksData from './SidebarLinksData'; | ||
|
|
||
| // Once the app finishes loading for the first time, we never show the skeleton again | ||
| // (even if isLoadingApp briefly flips back to true during a reconnect). | ||
| // This uses a module-level variable + connectWithoutView instead of a ref because | ||
| // a ref resets on unmount, so the skeleton would flash again when the component | ||
| // remounts (e.g. navigating between tabs). | ||
| let hasEverFinishedLoading = false; | ||
| Onyx.connectWithoutView({ | ||
| key: ONYXKEYS.IS_LOADING_APP, | ||
| callback: (value) => { | ||
| if (value !== false) { | ||
| return; | ||
| } | ||
| hasEverFinishedLoading = true; | ||
| }, | ||
| }); | ||
|
|
||
| function BaseSidebarScreen() { | ||
| const styles = useThemeStyles(); | ||
| const {translate} = useLocalize(); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); | ||
| const shouldDisplayLHB = !shouldUseNarrowLayout; | ||
|
|
||
| const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP); | ||
| const shouldShowSkeleton = isLoadingApp && !hasEverFinishedLoading; | ||
|
|
||
| // Must be called unconditionally so openApp() can proceed even when | ||
| // the skeleton is shown and SidebarLinksData has not mounted yet. | ||
| useConfirmReadyToOpenApp(); | ||
|
|
||
| return ( | ||
| <ScreenWrapper | ||
| shouldEnableKeyboardAvoidingView={false} | ||
|
|
@@ -30,9 +58,7 @@ function BaseSidebarScreen() { | |
| shouldDisplaySearch={shouldUseNarrowLayout} | ||
| shouldDisplayHelpButton={shouldUseNarrowLayout} | ||
| /> | ||
| <View style={[styles.flex1]}> | ||
| <SidebarLinksData insets={insets} /> | ||
| </View> | ||
| <View style={[styles.flex1]}>{shouldShowSkeleton ? <OptionsListSkeletonView shouldAnimate /> : <SidebarLinksData insets={insets} />}</View> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This conditional can deadlock app initialization on inbox-first routes: when Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — addressed in 32977a6. |
||
| {shouldDisplayLHB && <NavigationTabBar selectedTab={NAVIGATION_TABS.INBOX} />} | ||
| </> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest'; | |
| import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; | ||
| import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; | ||
| import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useStyleUtils from '@hooks/useStyleUtils'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
|
|
@@ -14,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; | |
| import {cancelSpan} from '@libs/telemetry/activeSpans'; | ||
| import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import ROUTES from '@src/ROUTES'; | ||
| import type {Report} from '@src/types/onyx'; | ||
|
|
||
|
|
@@ -24,20 +26,18 @@ type SidebarLinksProps = { | |
| /** List of options to display */ | ||
| optionListItems: Report[]; | ||
|
|
||
| /** Whether the reports are loading. When false it means they are ready to be used. */ | ||
| isLoading: OnyxEntry<boolean>; | ||
|
|
||
| /** The chat priority mode */ | ||
| priorityMode?: OnyxEntry<ValueOf<typeof CONST.PRIORITY_MODE>>; | ||
|
|
||
| /** Method to change currently active report */ | ||
| isActiveReport: (reportID: string) => boolean; | ||
| }; | ||
|
|
||
| function SidebarLinks({insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) { | ||
| function SidebarLinks({insets, optionListItems, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) { | ||
| const styles = useThemeStyles(); | ||
| const StyleUtils = useStyleUtils(); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); | ||
| const [isLoadingReportData = true] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); | ||
|
|
||
| useConfirmReadyToOpenApp(); | ||
|
|
||
|
|
@@ -90,7 +90,7 @@ function SidebarLinks({insets, optionListItems, isLoading, priorityMode = CONST. | |
| optionMode={viewMode} | ||
| onFirstItemRendered={setSidebarLoaded} | ||
| /> | ||
| {!!isLoading && optionListItems?.length === 0 && ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think optionListItems length is used here to avoid showing BlockingView(shouldShowEmptyLHN) Screen.Recording.2026-03-05.at.21.35.27.mov
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Restored the skeleton overlay in SidebarLinks using |
||
| {!!isLoadingReportData && optionListItems?.length === 0 && ( | ||
| <View style={[StyleSheet.absoluteFillObject, styles.appBG, styles.mt3]}> | ||
| <OptionsListSkeletonView shouldAnimate /> | ||
| </View> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are we doing this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isLoadingAppcan briefly flip back totrueduring a reconnect (e.g. network loss → reconnection). Without this latch, users would see the skeleton flash over an already-loaded LHN, which is a UX regression.hasEverFinishedLoadingensures the skeleton only appears on the very first app load and never again after that.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we doing this subscription outside of the view? Could we use a ref for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A ref resets to its initial value when the component unmounts. If
BaseSidebarScreenunmounts and remounts (e.g. navigating between tabs), a ref-based flag would go back tofalseand the skeleton could flash again even though the app already finished loading. A module-level variable survives independently of the component lifecycle, so it acts as a true one-time latch for the entire app session.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, I know that we typically try to avoid using connectWithoutView so could you add a similar comment to what you said above this so that people know why it was added?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 80bb9d1 — expanded the comment to explain why we use a module-level variable +
connectWithoutViewinstead of a ref.