Skip to content

Commit d3abbe7

Browse files
sgaczolCopilot
andauthored
refactor(Example): TabsContainer and StackContainer refactor (#3925)
## Description Previously, we were storing the actual React Component references directly inside the `useReducer` state (within `TabRoute` and `StackRoute` objects). This PR separates the navigation state from the Components. During the render phase, the containers dynamically resolve the correct Component by matching the route name from the state against a `componentsByName` Map (built via `useMemo` from the `routeConfigs` prop), rather than storing Component references in reducer state. There is one issue related to `StackContainer`: often in our environment, we wrap `StackContainer` in an intermediate component (e.g. `StackSetup`) to consume contexts such as `useToast()`. If that wrapper is not explicitly exported from its file, Fast Refresh causes `useReducer` inside `StackContainer` to re-fire its initialization function, resetting the stack to the first screen. This issue will be solved in a separate PR. ## Changes - Update `TabRoute` and `StackRoute` types to omit `Component` from the route state - Update `TabsContainer` and `StackContainer` to resolve components via a `componentsByName` Map during render ## Before & after - visual documentation N/A ## Test plan N/A ## Checklist - [ ] Included code example that can be used to test this change. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [ ] Ensured that CI passes --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]>
1 parent 150121e commit d3abbe7

9 files changed

Lines changed: 61 additions & 11 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { useMemo } from 'react';
2+
import type { StackRouteConfig } from '../stack';
3+
import type { TabRouteConfig } from '../tabs';
4+
5+
export const useComponentsByName = (
6+
routeConfigs: StackRouteConfig[] | TabRouteConfig[],
7+
) => {
8+
return useMemo(() => {
9+
const map = new Map<string, React.ComponentType>();
10+
11+
for (const config of routeConfigs) {
12+
map.set(config.name, config.Component);
13+
}
14+
15+
return map;
16+
}, [routeConfigs]);
17+
};

apps/src/shared/gamma/containers/stack/StackContainer.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ import {
2121
useRenderDebugInfo,
2222
} from 'react-native-screens/private';
2323
import { useParentNavigationEffect } from './hooks/useParentNavigationEffect';
24+
import { useComponentsByName } from '../shared/use-components-by-name';
2425

2526
export function StackContainer({ routeConfigs }: StackContainerProps) {
2627
useSanitizeRouteConfigs(routeConfigs);
2728

29+
const componentsByName = useComponentsByName(routeConfigs);
30+
2831
const [stackNavState, navActionDispatch]: [
2932
StackNavigationState,
3033
React.Dispatch<NavigationAction>,
@@ -62,7 +65,7 @@ export function StackContainer({ routeConfigs }: StackContainerProps) {
6265
return (
6366
<Stack.Host ref={hostRef}>
6467
{stackNavState.stack.map(
65-
({ Component, options: { headerConfig, ...options }, activityMode, routeKey }) => {
68+
({ options: { headerConfig, ...options }, activityMode, routeKey, name }) => {
6669
const stackNavigationContext: StackNavigationContextPayload = {
6770
routeKey,
6871
routeOptions: { ...options },
@@ -73,6 +76,13 @@ export function StackContainer({ routeConfigs }: StackContainerProps) {
7376
setRouteOptions: navMethods.setRouteOptions,
7477
};
7578

79+
const Component = componentsByName.get(name);
80+
if (!Component) {
81+
throw new Error(
82+
`[Stack] No config matches the "${name}" route name`,
83+
);
84+
}
85+
7686
return (
7787
<Stack.Screen
7888
key={routeKey}

apps/src/shared/gamma/containers/stack/StackContainer.types.tsx renamed to apps/src/shared/gamma/containers/stack/StackContainer.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ export type StackRouteConfig = {
2222
options: StackRouteOptions;
2323
};
2424

25-
export type StackRoute = StackRouteConfig & {
25+
export type StackRoute = Omit<StackRouteConfig, 'Component'> & {
2626
activityMode: StackScreenProps['activityMode'];
2727
routeKey: StackScreenProps['screenKey'];
28-
isMarkedForDismissal: Boolean; // whether this route is during or after dismissal process
28+
isMarkedForDismissal: boolean; // whether this route is during or after dismissal process
2929
};
3030

3131
/// StackContainer props

apps/src/shared/gamma/containers/stack/reducer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,10 @@ function createRouteFromConfig(
270270
config: StackRouteConfig,
271271
activityMode: StackScreenActivityMode = 'detached',
272272
): StackRoute {
273+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
274+
const { Component, ...rest } = config;
273275
return {
274-
...config,
276+
...rest,
275277
activityMode,
276278
routeKey: generateRouteKeyForRouteName(config.name),
277279
isMarkedForDismissal: false,

apps/src/shared/gamma/containers/tabs/TabsContainer.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from './reducer';
2222
import { RNSLog } from 'react-native-screens/private';
2323
import { TabsContainerItem } from './TabsContainerItem';
24+
import { useComponentsByName } from '../shared/use-components-by-name';
2425

2526
export function TabsContainer(props: TabsContainerProps) {
2627
RNSLog.info('TabsContainer render');
@@ -34,6 +35,8 @@ export function TabsContainer(props: TabsContainerProps) {
3435

3536
useSanitizeRouteConfigs(routeConfigs);
3637

38+
const componentsByName = useComponentsByName(routeConfigs);
39+
3740
const [tabsNavState, dispatch]: [
3841
TabsContainerState,
3942
React.Dispatch<TabsNavigationAction>,
@@ -84,7 +87,23 @@ export function TabsContainer(props: TabsContainerProps) {
8487
const pendingForUpdate =
8588
route.routeKey === tabsNavState.suggestedState.selectedRouteKey;
8689

87-
return <TabsContainerItem key={route.routeKey} route={route} navMethods={navMethods} isSelected={isSelected} pendingForUpdate={pendingForUpdate} />
90+
const Component = componentsByName.get(route.name);
91+
if (!Component) {
92+
throw new Error(
93+
`[Tabs] No route config matches the "${route.name}" route name`,
94+
);
95+
}
96+
97+
return (
98+
<TabsContainerItem
99+
key={route.routeKey}
100+
route={route}
101+
navMethods={navMethods}
102+
isSelected={isSelected}
103+
pendingForUpdate={pendingForUpdate}
104+
Component={Component}
105+
/>
106+
);
88107
})}
89108
</Tabs.Host>
90109
);

apps/src/shared/gamma/containers/tabs/TabsContainer.types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type TabRouteConfig = {
2727
/**
2828
* Runtime instance of a tab route. Created from a TabRouteConfig blueprint.
2929
*/
30-
export type TabRoute = TabRouteConfig & {
30+
export type TabRoute = Omit<TabRouteConfig, 'Component'> & {
3131
routeKey: string;
3232
};
3333

apps/src/shared/gamma/containers/tabs/TabsContainerItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function TabsContainerItemImpl(props: TabsContainerItemProps) {
3737
{...nativeOptions}
3838
screenKey={screenKey}>
3939
<TabsNavigationContext value={tabsNavigationContext}>
40-
{getContent(props.route.Component, safeAreaConfiguration)}
40+
{getContent(props.Component, safeAreaConfiguration)}
4141
</TabsNavigationContext>
4242
</Tabs.Screen>
4343
);
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { TabRoute, TabsNavigationMethods } from './TabsContainer.types';
2+
import React from 'react';
23

34
export type TabsContainerItemProps = {
45
route: TabRoute;
56
navMethods: TabsNavigationMethods;
67
isSelected: boolean;
78
pendingForUpdate: boolean;
8-
}
9-
9+
Component: React.ComponentType;
10+
};

apps/src/shared/gamma/containers/tabs/reducer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ function tabsActionSetOptionsHandler(
135135
}
136136

137137
function createTabRouteFromConfig(config: TabRouteConfig): TabRoute {
138+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
139+
const { Component, ...rest } = config;
138140
return {
139-
...config,
141+
...rest,
140142
// Tab names are required to be unique (enforced by useSanitizeRouteConfigs),
141143
// so the name itself serves as a stable unique key.
142144
routeKey: config.name,
@@ -205,4 +207,3 @@ function navStateWithConfirmedState(
205207
suggestedState: state.suggestedState,
206208
};
207209
}
208-

0 commit comments

Comments
 (0)