Skip to content

Commit c488834

Browse files
committed
perf: stop WorkspacesTabButton re-rendering on every navigation
1 parent b1ba0a2 commit c488834

3 files changed

Lines changed: 90 additions & 161 deletions

File tree

Lines changed: 59 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,90 @@
1-
import {useCallback} from 'react';
2-
import type {OnyxCollection} from 'react-native-onyx';
1+
import type {NavigationState, PartialState} from '@react-navigation/native';
32
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
43
import {isFullScreenName, isWorkspaceNavigatorRouteName} from '@libs/Navigation/helpers/isNavigatorName';
54
import {getWorkspacesTabStateFromSessionStorage} from '@libs/Navigation/helpers/lastVisitedTabPathUtils';
65
import navigateToWorkspacesPage from '@libs/Navigation/helpers/navigateToWorkspacesPage';
76
import {getTabState} from '@libs/Navigation/helpers/tabNavigatorUtils';
7+
import navigationRef from '@libs/Navigation/navigationRef';
88
import type {DomainSplitNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
99
import NAVIGATORS from '@src/NAVIGATORS';
10-
import ONYXKEYS from '@src/ONYXKEYS';
1110
import type SCREENS from '@src/SCREENS';
12-
import type {Domain, Policy} from '@src/types/onyx';
13-
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
14-
import useOnyx from './useOnyx';
1511
import useResponsiveLayout from './useResponsiveLayout';
16-
import useRootNavigationState from './useRootNavigationState';
12+
13+
type WorkspaceRouteType = NavigationState['routes'][number] | NonNullable<PartialState<NavigationState>['routes']>[number];
14+
type WorkspaceParams = WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL] | DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL];
15+
16+
/**
17+
* Walks the root nav state to find the last workspace/domain route the user had open.
18+
* Falls back to session storage when no live workspace route exists.
19+
*
20+
* Multiple TAB_NAVIGATOR instances can coexist in the root stack — when navigation from
21+
* inside an RHP targets a tab, linkTo PUSHes a fresh TabNavigator above the modal, and that
22+
* new instance's WORKSPACE_NAVIGATOR slot starts empty. Older instances kept alive by
23+
* ensureTabNavigatorRoutes still hold the previous workspace state, so flatten every
24+
* workspace route from every TabNavigator in stack order and take the most recent one.
25+
*/
26+
function findLastWorkspaceRoute(rootState: NavigationState | undefined): WorkspaceRouteType | undefined {
27+
const topmostFullScreenRoute = rootState?.routes?.findLast((route) => isFullScreenName(route.name));
28+
if (!topmostFullScreenRoute) {
29+
return undefined;
30+
}
31+
32+
const lastWorkspaceRoute = (rootState?.routes ?? [])
33+
.filter((route) => route.name === NAVIGATORS.TAB_NAVIGATOR)
34+
.flatMap((tabNavigatorRoute) => {
35+
const workspaceNavigatorRoute = getTabState(tabNavigatorRoute)?.routes?.find((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR);
36+
const workspaceNavigatorState = workspaceNavigatorRoute?.state ?? (workspaceNavigatorRoute?.key ? getPreservedNavigatorState(workspaceNavigatorRoute.key) : undefined);
37+
return workspaceNavigatorState?.routes?.filter((route) => isWorkspaceNavigatorRouteName(route.name)) ?? [];
38+
})
39+
.at(-1);
40+
41+
if (lastWorkspaceRoute) {
42+
return lastWorkspaceRoute;
43+
}
44+
45+
return getWorkspacesTabStateFromSessionStorage()
46+
?.routes?.findLast((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR)
47+
?.state?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
48+
}
49+
50+
function getWorkspacesTabState(route: WorkspaceRouteType | undefined): NavigationState | PartialState<NavigationState> | undefined {
51+
if (!route) {
52+
return undefined;
53+
}
54+
return route.state ?? (route.key ? getPreservedNavigatorState(route.key) : undefined);
55+
}
1756

1857
/**
1958
* The Workspaces tab can show three things: the workspaces list, a specific workspace page,
2059
* or a specific domain page. When the user navigates away and comes back to the tab,
2160
* this hook ensures they return to whichever of those they had open last — not always the list.
2261
*
23-
* It resolves the last visited route from navigation state, fetches the matching policy/domain
24-
* from Onyx (to verify it's still accessible), and returns a callback that performs the navigation.
62+
* Resolves nav state and route IDs at click time inside the returned callback so the hook
63+
* has no reactive subscriptions to nav state — unrelated navigations (e.g. opening a report)
64+
* don't trigger re-renders. The destination workspace/domain page handles invalid IDs.
2565
*/
2666
function useRestoreWorkspacesTabOnNavigate() {
2767
const {shouldUseNarrowLayout} = useResponsiveLayout();
28-
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
2968

30-
// Find the last route the user had open in the Workspaces tab (workspace, domain, or list).
31-
// Priority: live nav state (root level) -> inside TabNavigator -> preserved state -> session storage.
32-
const routeState = useRootNavigationState((rootState) => {
69+
return () => {
70+
const rootState = navigationRef.isReady() ? navigationRef.getRootState() : undefined;
3371
const topmostFullScreenRoute = rootState?.routes?.findLast((route) => isFullScreenName(route.name));
34-
if (!topmostFullScreenRoute) {
35-
return {};
36-
}
37-
38-
// Multiple TAB_NAVIGATOR instances can coexist in the root stack — when navigation from
39-
// inside an RHP targets a tab, linkTo PUSHes a fresh TabNavigator above the modal, and that
40-
// new instance's WORKSPACE_NAVIGATOR slot starts empty. Older instances kept alive by
41-
// ensureTabNavigatorRoutes still hold the previous workspace state, so flatten every
42-
// workspace route from every TabNavigator in stack order and take the most recent one.
43-
const lastWorkspaceRoute = (rootState?.routes ?? [])
44-
.filter((route) => route.name === NAVIGATORS.TAB_NAVIGATOR)
45-
.flatMap((tabNavigatorRoute) => {
46-
const workspaceNavigatorRoute = getTabState(tabNavigatorRoute)?.routes?.find((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR);
47-
const workspaceNavigatorState = workspaceNavigatorRoute?.state ?? (workspaceNavigatorRoute?.key ? getPreservedNavigatorState(workspaceNavigatorRoute.key) : undefined);
48-
return workspaceNavigatorState?.routes?.filter((route) => isWorkspaceNavigatorRouteName(route.name)) ?? [];
49-
})
50-
.at(-1);
51-
52-
if (lastWorkspaceRoute) {
53-
const tabState = lastWorkspaceRoute.state ?? (lastWorkspaceRoute.key ? getPreservedNavigatorState(lastWorkspaceRoute.key) : undefined);
54-
return {lastWorkspacesTabNavigatorRoute: lastWorkspaceRoute, workspacesTabState: tabState, topmostFullScreenRoute};
55-
}
56-
57-
// Fall back to session storage when no workspace route exists anywhere in the navigation tree.
58-
const sessionRoute = getWorkspacesTabStateFromSessionStorage()
59-
?.routes?.findLast((route) => route.name === NAVIGATORS.WORKSPACE_NAVIGATOR)
60-
?.state?.routes?.findLast((route) => isWorkspaceNavigatorRouteName(route.name));
61-
if (sessionRoute) {
62-
return {lastWorkspacesTabNavigatorRoute: sessionRoute, workspacesTabState: sessionRoute.state, topmostFullScreenRoute};
63-
}
64-
65-
return {topmostFullScreenRoute};
66-
});
67-
68-
const {lastWorkspacesTabNavigatorRoute, workspacesTabState, topmostFullScreenRoute} = routeState;
69-
70-
// If the last route was a specific workspace or domain, extract its ID from params
71-
const params = workspacesTabState?.routes?.at(0)?.params as
72-
| WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]
73-
| DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL];
74-
const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
75-
const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
76-
77-
// Fetch the policy/domain to verify it's still accessible (not deleted/hidden) before restoring
78-
const lastViewedPolicySelector = useCallback(
79-
(policies: OnyxCollection<Policy>) => {
80-
if (lastWorkspacesTabNavigatorRoute?.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) {
81-
return undefined;
82-
}
83-
return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`];
84-
},
85-
[lastWorkspacesTabNavigatorRoute?.name, paramsPolicyID],
86-
);
87-
const [lastViewedPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: lastViewedPolicySelector});
72+
const lastWorkspacesTabNavigatorRoute = findLastWorkspaceRoute(rootState);
73+
const workspacesTabState = getWorkspacesTabState(lastWorkspacesTabNavigatorRoute);
8874

89-
const lastViewedDomainSelector = useCallback(
90-
(domains: OnyxCollection<Domain>) => {
91-
if (lastWorkspacesTabNavigatorRoute?.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) {
92-
return undefined;
93-
}
94-
return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`];
95-
},
96-
[lastWorkspacesTabNavigatorRoute?.name, paramsDomainAccountID],
97-
);
98-
const [lastViewedDomain] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN, {selector: lastViewedDomainSelector});
75+
const params = workspacesTabState?.routes?.at(0)?.params as WorkspaceParams | undefined;
76+
const policyID = params && 'policyID' in params ? params.policyID : undefined;
77+
const domainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
9978

100-
return useCallback(() => {
10179
navigateToWorkspacesPage({
10280
shouldUseNarrowLayout,
103-
currentUserLogin,
104-
policy: lastViewedPolicy,
105-
domain: lastViewedDomain,
81+
policyID,
82+
domainAccountID,
10683
lastWorkspacesTabNavigatorRoute,
10784
topmostFullScreenRoute,
10885
workspacesTabState,
10986
});
110-
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState]);
87+
};
11188
}
11289

11390
export default useRestoreWorkspacesTabOnNavigate;

src/libs/Navigation/helpers/navigateToWorkspacesPage.ts

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser';
44
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
55
import Navigation from '@libs/Navigation/Navigation';
66
import navigationRef from '@libs/Navigation/navigationRef';
7-
import {isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
87
import NAVIGATORS from '@src/NAVIGATORS';
98
import ROUTES from '@src/ROUTES';
109
import type {Route} from '@src/ROUTES';
1110
import SCREENS from '@src/SCREENS';
12-
import type {Domain, Policy} from '@src/types/onyx';
1311
import getActiveTabName from './getActiveTabName';
1412
import getPathFromState from './getPathFromState';
1513
import {getTabState} from './tabNavigatorUtils';
@@ -26,10 +24,9 @@ function wrapStateInNavigators(state: PartialState<NavigationState>, navigators:
2624
}
2725

2826
type Params = {
29-
currentUserLogin?: string;
3027
shouldUseNarrowLayout: boolean;
31-
policy?: Policy;
32-
domain?: Domain;
28+
policyID?: string;
29+
domainAccountID?: number;
3330
lastWorkspacesTabNavigatorRoute?: RouteType;
3431
topmostFullScreenRoute?: RouteType;
3532
/**
@@ -43,7 +40,7 @@ type Params = {
4340
workspacesTabState?: NavigationState | PartialState<NavigationState>;
4441
};
4542

46-
const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState}: Params) => {
43+
const navigateToWorkspacesPage = ({shouldUseNarrowLayout, policyID, domainAccountID, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState}: Params) => {
4744
const rootState = navigationRef.getRootState();
4845
const focusedRoute = rootState ? findFocusedRoute(rootState) : undefined;
4946
const isOnWorkspacesList = focusedRoute?.name === SCREENS.WORKSPACES_LIST;
@@ -93,45 +90,40 @@ const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, poli
9390
return;
9491
}
9592

96-
// Workspace route found: try to restore last workspace screen.
93+
// Workspace route found: navigate to last workspace screen by ID.
94+
// Validation is deferred to the destination — if the policy was deleted or access
95+
// revoked, the workspace page will redirect to the list itself.
9796
if (lastWorkspacesTabNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
98-
const shouldShowPolicy = shouldShowPolicyUtil(policy, false, currentUserLogin);
99-
const isPendingDelete = isPendingDeletePolicy(policy);
100-
101-
// Workspace is not accessible or is being deleted: go to list.
102-
if (!shouldShowPolicy || isPendingDelete) {
97+
if (!policyID) {
10398
Navigation.navigate(ROUTES.WORKSPACES_LIST.route);
10499
return;
105100
}
106-
107-
if (policy?.id) {
108-
// Synthesize a URL from the captured WorkspaceSplitNavigator inner state and navigate
109-
// to it. URL-based navigation goes through `getStateFromPath`, which produces a fully
110-
// formed nested state and reliably handles pushing a fresh TabNavigator on top of an
111-
// existing fullscreen stack. The state has to be wrapped with its full ancestor chain
112-
// (TAB_NAVIGATOR > WORKSPACE_NAVIGATOR > WORKSPACE_SPLIT_NAVIGATOR) so `getPathFromState`
113-
// can match the linking-config hierarchy and produce a real URL like
114-
// `/workspaces/POLICY_ID/workflows`; otherwise the resolver falls back to navigator
115-
// names as path segments and the result hits 404. Narrow layouts skip the deep-restore
116-
// and go to the workspace's initial page (mirrors mobile behavior).
117-
const wrappedState =
118-
!shouldUseNarrowLayout && workspacesTabState
119-
? wrapStateInNavigators(workspacesTabState as PartialState<NavigationState>, [
120-
NAVIGATORS.TAB_NAVIGATOR,
121-
NAVIGATORS.WORKSPACE_NAVIGATOR,
122-
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
123-
])
124-
: undefined;
125-
const targetPath = (wrappedState ? getPathFromState(wrappedState) : ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)) as Route;
126-
Navigation.navigate(targetPath);
127-
}
101+
// Synthesize a URL from the captured WorkspaceSplitNavigator inner state and navigate
102+
// to it. URL-based navigation goes through `getStateFromPath`, which produces a fully
103+
// formed nested state and reliably handles pushing a fresh TabNavigator on top of an
104+
// existing fullscreen stack. The state has to be wrapped with its full ancestor chain
105+
// (TAB_NAVIGATOR > WORKSPACE_NAVIGATOR > WORKSPACE_SPLIT_NAVIGATOR) so `getPathFromState`
106+
// can match the linking-config hierarchy and produce a real URL like
107+
// `/workspaces/POLICY_ID/workflows`; otherwise the resolver falls back to navigator
108+
// names as path segments and the result hits 404. Narrow layouts skip the deep-restore
109+
// and go to the workspace's initial page (mirrors mobile behavior).
110+
const wrappedState =
111+
!shouldUseNarrowLayout && workspacesTabState
112+
? wrapStateInNavigators(workspacesTabState as PartialState<NavigationState>, [
113+
NAVIGATORS.TAB_NAVIGATOR,
114+
NAVIGATORS.WORKSPACE_NAVIGATOR,
115+
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
116+
])
117+
: undefined;
118+
const targetPath = (wrappedState ? getPathFromState(wrappedState) : ROUTES.WORKSPACE_INITIAL.getRoute(policyID)) as Route;
119+
Navigation.navigate(targetPath);
128120
return;
129121
}
130122

131-
// Domain route found: try to restore last domain screen.
123+
// Domain route found: navigate to last domain screen by ID.
132124
if (lastWorkspacesTabNavigatorRoute.name === NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR) {
133-
if (domain?.accountID !== undefined) {
134-
Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domain.accountID));
125+
if (domainAccountID !== undefined) {
126+
Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domainAccountID));
135127
return;
136128
}
137129
}

0 commit comments

Comments
 (0)