Skip to content

Commit 2492c9a

Browse files
committed
refactor: Change COmponent to element, add tests
1 parent 5767b00 commit 2492c9a

17 files changed

Lines changed: 729 additions & 258 deletions

apps/src/shared/containers/shared/use-components-by-name.ts renamed to apps/src/shared/containers/shared/use-elements-by-name.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import React, { useMemo } from 'react';
22
import type { StackRouteConfig } from '../stack';
33
import type { TabRouteConfig } from '../tabs';
44

5-
export const useComponentsByName = (
5+
export const useElementsByName = (
66
routeConfigs: StackRouteConfig[] | TabRouteConfig[],
77
) => {
88
return useMemo(() => {
9-
const map = new Map<string, React.ComponentType>();
9+
const map = new Map<string, React.ReactElement>();
1010

1111
for (const config of routeConfigs) {
12-
map.set(config.name, config.Component);
12+
map.set(config.name, config.element);
1313
}
1414

1515
return map;

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import {
2121
useRenderDebugInfo,
2222
} from 'react-native-screens/private';
2323
import { useParentNavigationEffect } from './hooks/useParentNavigationEffect';
24-
import { useComponentsByName } from '../shared/use-components-by-name';
24+
import { useElementsByName } from '../shared/use-elements-by-name';
2525

2626
export function StackContainer({ routeConfigs }: StackContainerProps) {
2727
useSanitizeRouteConfigs(routeConfigs);
2828

29-
const componentsByName = useComponentsByName(routeConfigs);
29+
const elementsByName = useElementsByName(routeConfigs);
3030

3131
const [stackNavState, navActionDispatch]: [
3232
StackNavigationState,
@@ -81,8 +81,8 @@ export function StackContainer({ routeConfigs }: StackContainerProps) {
8181
setRouteOptions: navMethods.setRouteOptions,
8282
};
8383

84-
const Component = componentsByName.get(name);
85-
if (!Component) {
84+
const element = elementsByName.get(name);
85+
if (!element) {
8686
throw new Error(
8787
`[Stack] No config matches the "${name}" route name`,
8888
);
@@ -97,7 +97,9 @@ export function StackContainer({ routeConfigs }: StackContainerProps) {
9797
onDismiss={onScreenDismissed}
9898
onNativeDismiss={onScreenNativelyDismissed}>
9999
<StackNavigationContext.Provider value={stackNavigationContext}>
100-
<Component />
100+
{/* Clone so multiple stack entries of the same route get distinct instances.
101+
{React.cloneElement(element)} */}
102+
{element}
101103
{headerConfig !== undefined && (
102104
<Stack.HeaderConfig ref={headerConfigRef} {...headerConfig} />
103105
)}

apps/src/shared/containers/stack/StackContainer.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ export type StackRouteOptions = Omit<
2020
*/
2121
export type StackRouteConfig = {
2222
name: string;
23-
Component: React.ComponentType;
23+
element: React.ReactElement;
2424
options: StackRouteOptions;
2525
};
2626

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ function createRouteFromConfig(
277277
activityMode: StackScreenActivityMode = 'detached',
278278
): StackRoute {
279279
// eslint-disable-next-line @typescript-eslint/no-unused-vars
280-
const { Component, ...rest } = config;
280+
const { element, ...rest } = config;
281281
return {
282282
...rest,
283283
activityMode,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +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';
24+
import { useElementsByName } from '../shared/use-elements-by-name';
2525

2626
export function TabsContainer(props: TabsContainerProps) {
2727
RNSLog.info('TabsContainer render');
@@ -30,7 +30,7 @@ export function TabsContainer(props: TabsContainerProps) {
3030

3131
useSanitizeRouteConfigs(routeConfigs);
3232

33-
const componentsByName = useComponentsByName(routeConfigs);
33+
const elementsByName = useElementsByName(routeConfigs);
3434

3535
const [tabsNavState, dispatch]: [
3636
TabsContainerState,
@@ -82,8 +82,8 @@ export function TabsContainer(props: TabsContainerProps) {
8282
const pendingForUpdate =
8383
route.routeKey === tabsNavState.suggestedState.selectedRouteKey;
8484

85-
const Component = componentsByName.get(route.name);
86-
if (!Component) {
85+
const element = elementsByName.get(route.name);
86+
if (!element) {
8787
throw new Error(
8888
`[Tabs] No route config matches the "${route.name}" route name`,
8989
);
@@ -96,7 +96,7 @@ export function TabsContainer(props: TabsContainerProps) {
9696
navMethods={navMethods}
9797
isSelected={isSelected}
9898
pendingForUpdate={pendingForUpdate}
99-
Component={Component}
99+
element={element}
100100
/>
101101
);
102102
})}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export type TabRouteOptions = Omit<
2020
*/
2121
export type TabRouteConfig = {
2222
name: string;
23-
Component: React.ComponentType;
23+
element: React.ReactElement;
2424
options?: TabRouteOptions;
2525
};
2626

2727
/**
2828
* Runtime instance of a tab route. Created from a TabRouteConfig blueprint.
2929
*/
30-
export type TabRoute = Omit<TabRouteConfig, 'Component'> & {
30+
export type TabRoute = Omit<TabRouteConfig, 'element'> & {
3131
routeKey: string;
3232
};
3333

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ function TabsContainerItemImpl(props: TabsContainerItemProps) {
4343
return (
4444
<Tabs.Screen key={screenKey} {...nativeOptions} screenKey={screenKey}>
4545
<TabsNavigationContext value={tabsNavigationContext}>
46-
{getContent(props.Component, safeAreaConfiguration)}
46+
{getContent(props.element, safeAreaConfiguration)}
4747
</TabsNavigationContext>
4848
</Tabs.Screen>
4949
);
5050
}
5151

5252
function getContent(
53-
Component: TabRouteConfig['Component'],
53+
element: TabRouteConfig['element'],
5454
safeAreaConfiguration: SafeAreaViewProps | undefined,
5555
) {
5656
const safeAreaConfigurationWithDefault = getSafeAreaViewEdges(
@@ -62,14 +62,10 @@ function getContent(
6262
);
6363

6464
if (anySAVEdgeSet) {
65-
return (
66-
<SafeAreaView {...safeAreaConfiguration}>
67-
<Component />
68-
</SafeAreaView>
69-
);
65+
return <SafeAreaView {...safeAreaConfiguration}>{element}</SafeAreaView>;
7066
}
7167

72-
return <Component />;
68+
return element;
7369
}
7470

7571
function getSafeAreaViewEdges(

apps/src/shared/containers/tabs/TabsContainerItem.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ export type TabsContainerItemProps = {
66
navMethods: TabsNavigationMethods;
77
isSelected: boolean;
88
pendingForUpdate: boolean;
9-
Component: React.ComponentType;
9+
element: React.ReactElement;
1010
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ function tabsActionSetOptionsHandler(
139139

140140
function createTabRouteFromConfig(config: TabRouteConfig): TabRoute {
141141
// eslint-disable-next-line @typescript-eslint/no-unused-vars
142-
const { Component, ...rest } = config;
142+
const { element, ...rest } = config;
143143
return {
144144
...rest,
145145
// Tab names are required to be unique (enforced by useSanitizeRouteConfigs),
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React, { useState } from 'react';
2+
import { Button, StyleSheet, Text, View } from 'react-native';
3+
import {
4+
StackContainer,
5+
useStackNavigationContext,
6+
} from '@apps/shared/containers/stack';
7+
import { CenteredLayoutView } from '@apps/shared/CenteredLayoutView';
8+
9+
const ROUTE_NAMES = [
10+
'SharedOne',
11+
'SharedTwo',
12+
'InlineOne',
13+
'InlineTwo',
14+
] as const;
15+
16+
function StatefulScreen({
17+
title,
18+
backgroundColor,
19+
}: {
20+
title: string;
21+
backgroundColor: string;
22+
}) {
23+
const [count, setCount] = useState(0);
24+
const navigation = useStackNavigationContext();
25+
26+
return (
27+
<CenteredLayoutView style={{ backgroundColor }}>
28+
<Text style={styles.title}>{title}</Text>
29+
<Text style={styles.meta}>routeKey: {navigation.routeKey}</Text>
30+
<Text style={styles.counter}>Local count: {count}</Text>
31+
32+
<Button title="+ Local state" onPress={() => setCount(c => c + 1)} />
33+
34+
<View style={styles.row}>
35+
{ROUTE_NAMES.map(name => (
36+
<Button
37+
key={name}
38+
title={`Push ${name}`}
39+
onPress={() => navigation.push(name)}
40+
/>
41+
))}
42+
</View>
43+
44+
<Button title="Pop" onPress={() => navigation.pop(navigation.routeKey)} />
45+
</CenteredLayoutView>
46+
);
47+
}
48+
49+
// Same element object reused by SharedOne + SharedTwo — exercises StackContainer
50+
// rendering without cloneElement (state/identity sharing across stack entries).
51+
const SharedRef = (
52+
<StatefulScreen title="Shared Ref (same object)" backgroundColor="#E6E6FA" />
53+
);
54+
55+
export default function TestStackPoC() {
56+
return (
57+
<StackContainer
58+
routeConfigs={[
59+
{
60+
name: 'SharedOne',
61+
element: SharedRef,
62+
options: {},
63+
},
64+
{
65+
name: 'SharedTwo',
66+
element: SharedRef,
67+
options: {},
68+
},
69+
{
70+
name: 'InlineOne',
71+
element: (
72+
<StatefulScreen title="Inline One" backgroundColor="#FFE4C4" />
73+
),
74+
options: {},
75+
},
76+
{
77+
name: 'InlineTwo',
78+
element: (
79+
<StatefulScreen title="Inline Two" backgroundColor="#FFE4C4" />
80+
),
81+
options: {},
82+
},
83+
]}
84+
/>
85+
);
86+
}
87+
88+
const styles = StyleSheet.create({
89+
title: { fontSize: 20, fontWeight: 'bold' },
90+
meta: { fontSize: 14, marginVertical: 4 },
91+
counter: { fontSize: 24, fontWeight: '600', marginVertical: 8 },
92+
row: { gap: 4, alignItems: 'stretch', width: '100%', paddingHorizontal: 16 },
93+
});

0 commit comments

Comments
 (0)