Skip to content

Commit

Permalink
fix: strongly type the component prop on RouteConfigComponent (#1…
Browse files Browse the repository at this point in the history
…0519)

Having a Route Parameters system is good and `navigator.navigate` can type check just fine, but the screen itself is not guaranteed to match the correct route params signature. These changes will allow screens to declare a `route: RouteProp<RouteParams, 'route'>` prop and get type-checked properly.
  • Loading branch information
thomasttvo authored and satya164 committed Nov 27, 2022
1 parent a76db87 commit 8850c51
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 146 deletions.
2 changes: 1 addition & 1 deletion example/src/Screens/AuthFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const SignInScreen = ({

const HomeScreen = ({
navigation,
}: StackScreenProps<AuthStackParams, 'SignIn'>) => {
}: StackScreenProps<AuthStackParams, 'Home'>) => {
const { signOut } = React.useContext(AuthContext);

return (
Expand Down
14 changes: 6 additions & 8 deletions example/src/Screens/LinkComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ import {
createStackNavigator,
StackScreenProps,
} from '@react-navigation/stack';
import type { LinkComponentDemoParamList } from 'example/src/screens';
import * as React from 'react';
import { Platform, ScrollView, StyleSheet, View } from 'react-native';
import { Button } from 'react-native-paper';

import Albums from '../Shared/Albums';
import Article from '../Shared/Article';

type SimpleStackParams = {
Article: { author: string };
Albums: undefined;
};

const scrollEnabled = Platform.select({ web: true, default: false });

const LinkButton = ({
Expand All @@ -35,7 +31,7 @@ const LinkButton = ({
const ArticleScreen = ({
navigation,
route,
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
}: StackScreenProps<LinkComponentDemoParamList, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
Expand Down Expand Up @@ -82,7 +78,9 @@ const ArticleScreen = ({
);
};

const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
const AlbumsScreen = ({
navigation,
}: StackScreenProps<LinkComponentDemoParamList>) => {
return (
<ScrollView>
<View style={styles.buttons}>
Expand Down Expand Up @@ -112,7 +110,7 @@ const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
);
};

const SimpleStack = createStackNavigator<SimpleStackParams>();
const SimpleStack = createStackNavigator<LinkComponentDemoParamList>();

type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
StackScreenProps<ParamListBase>;
Expand Down
3 changes: 2 additions & 1 deletion example/src/Screens/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { StackScreenProps } from '@react-navigation/stack';
import type { RootStackParamList } from 'example/src/screens';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-paper';

const NotFoundScreen = ({
route,
navigation,
}: StackScreenProps<{ Home: undefined }>) => {
}: StackScreenProps<RootStackParamList, 'NotFound'>) => {
return (
<View style={styles.container}>
<Text style={styles.title}>404 Not Found ({route.path})</Text>
Expand Down
141 changes: 18 additions & 123 deletions example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
DefaultTheme,
InitialState,
NavigationContainer,
NavigatorScreenParams,
PathConfigMap,
useNavigationContainerRef,
} from '@react-navigation/native';
Expand Down Expand Up @@ -46,117 +45,19 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';

import { restartApp } from './Restart';
import AuthFlow from './Screens/AuthFlow';
import BottomTabs from './Screens/BottomTabs';
import DynamicTabs from './Screens/DynamicTabs';
import LinkComponent from './Screens/LinkComponent';
import MasterDetail from './Screens/MasterDetail';
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MixedHeaderMode from './Screens/MixedHeaderMode';
import MixedStack from './Screens/MixedStack';
import ModalStack from './Screens/ModalStack';
import NativeStack from './Screens/NativeStack';
import NativeStackHeaderCustomization from './Screens/NativeStackHeaderCustomization';
import NativeStackPreventRemove from './Screens/NativeStackPreventRemove';
import {
RootDrawerParamList,
RootStackParamList,
SCREEN_NAMES,
SCREENS,
} from './screens';
import NotFound from './Screens/NotFound';
import SimpleStack from './Screens/SimpleStack';
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
import StackPreventRemove from './Screens/StackPreventRemove';
import StackTransparent from './Screens/StackTransparent';
import SettingsItem from './Shared/SettingsItem';

if (Platform.OS !== 'web') {
LogBox.ignoreLogs(['Require cycle:']);
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}

type RootDrawerParamList = {
Examples: undefined;
};

const SCREENS = {
NativeStack: { title: 'Native Stack', component: NativeStack },
SimpleStack: { title: 'Simple Stack', component: SimpleStack },
ModalStack: {
title: 'Modal Stack',
component: ModalStack,
},
MixedStack: {
title: 'Regular + Modal Stack',
component: MixedStack,
},
MixedHeaderMode: {
title: 'Float + Screen Header Stack',
component: MixedHeaderMode,
},
StackTransparent: {
title: 'Transparent Stack',
component: StackTransparent,
},
StackHeaderCustomization: {
title: 'Header Customization in Stack',
component: StackHeaderCustomization,
},
NativeStackHeaderCustomization: {
title: 'Header Customization in Native Stack',
component: NativeStackHeaderCustomization,
},
BottomTabs: { title: 'Bottom Tabs', component: BottomTabs },
MaterialTopTabs: {
title: 'Material Top Tabs',
component: MaterialTopTabsScreen,
},
MaterialBottomTabs: {
title: 'Material Bottom Tabs',
component: MaterialBottomTabs,
},
DynamicTabs: {
title: 'Dynamic Tabs',
component: DynamicTabs,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
},
StackPreventRemove: {
title: 'Prevent removing screen in Stack',
component: StackPreventRemove,
},
NativeStackPreventRemove: {
title: 'Prevent removing screen in Native Stack',
component: NativeStackPreventRemove,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
},
};

type RootStackParamList = {
Home: NavigatorScreenParams<RootDrawerParamList>;
NotFound: undefined;
} & {
[P in keyof typeof SCREENS]: NavigatorScreenParams<{
Article: { author?: string };
Albums: undefined;
Chat: undefined;
Contacts: undefined;
NewsFeed: undefined;
Dialog: undefined;
}>;
};

const Drawer = createDrawerNavigator<RootDrawerParamList>();
const Stack = createStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -267,9 +168,7 @@ export default function App() {
prefixes: [createURL('/')],
config: {
initialRouteName: 'Home',
screens: (Object.keys(SCREENS) as (keyof typeof SCREENS)[]).reduce<
PathConfigMap<RootStackParamList>
>(
screens: SCREEN_NAMES.reduce<PathConfigMap<RootStackParamList>>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
const path = name
Expand Down Expand Up @@ -381,20 +280,16 @@ export default function App() {
}}
/>
<Divider />
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => {
// FIXME: figure this out later
// @ts-expect-error
navigation.navigate(name);
}}
/>
)
)}
{SCREEN_NAMES.map((name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => {
navigation.navigate(name);
}}
/>
))}
</SafeAreaView>
</ScrollView>
)}
Expand All @@ -407,7 +302,7 @@ export default function App() {
component={NotFound}
options={{ title: 'Oops!' }}
/>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map((name) => (
{SCREEN_NAMES.map((name) => (
<Stack.Screen
key={name}
name={name}
Expand Down
137 changes: 137 additions & 0 deletions example/src/screens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { NavigatorScreenParams } from '@react-navigation/native';

import AuthFlow from './Screens/AuthFlow';
import BottomTabs from './Screens/BottomTabs';
import DynamicTabs from './Screens/DynamicTabs';
import LinkComponent from './Screens/LinkComponent';
import MasterDetail from './Screens/MasterDetail';
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MixedHeaderMode from './Screens/MixedHeaderMode';
import MixedStack from './Screens/MixedStack';
import ModalStack from './Screens/ModalStack';
import NativeStack from './Screens/NativeStack';
import NativeStackHeaderCustomization from './Screens/NativeStackHeaderCustomization';
import NativeStackPreventRemove from './Screens/NativeStackPreventRemove';
import SimpleStack from './Screens/SimpleStack';
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
import StackPreventRemove from './Screens/StackPreventRemove';
import StackTransparent from './Screens/StackTransparent';

export type RootDrawerParamList = {
Examples: undefined;
};

export type LinkComponentDemoParamList = {
Article: { author: string };
Albums: undefined;
};

const paramsType = <params,>() => undefined as unknown as params;

export const SCREENS = {
NativeStack: {
title: 'Native Stack',
component: NativeStack,
params: undefined,
},
SimpleStack: {
title: 'Simple Stack',
component: SimpleStack,
params: undefined,
},
ModalStack: {
title: 'Modal Stack',
component: ModalStack,
params: undefined,
},
MixedStack: {
title: 'Regular + Modal Stack',
component: MixedStack,
params: undefined,
},
MixedHeaderMode: {
title: 'Float + Screen Header Stack',
component: MixedHeaderMode,
params: undefined,
},
StackTransparent: {
title: 'Transparent Stack',
component: StackTransparent,
params: undefined,
},
StackHeaderCustomization: {
title: 'Header Customization in Stack',
component: StackHeaderCustomization,
params: undefined,
},
NativeStackHeaderCustomization: {
title: 'Header Customization in Native Stack',
component: NativeStackHeaderCustomization,
params: undefined,
},
BottomTabs: {
title: 'Bottom Tabs',
component: BottomTabs,
params: undefined,
},
MaterialTopTabs: {
title: 'Material Top Tabs',
component: MaterialTopTabsScreen,
params: undefined,
},
MaterialBottomTabs: {
title: 'Material Bottom Tabs',
component: MaterialBottomTabs,
params: undefined,
},
DynamicTabs: {
title: 'Dynamic Tabs',
component: DynamicTabs,
params: undefined,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
params: undefined,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
params: undefined,
},
StackPreventRemove: {
title: 'Prevent removing screen in Stack',
component: StackPreventRemove,
params: undefined,
},
NativeStackPreventRemove: {
title: 'Prevent removing screen in Native Stack',
component: NativeStackPreventRemove,
params: undefined,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
params: paramsType<
NavigatorScreenParams<LinkComponentDemoParamList> | undefined
>(),
},
};

export const SCREEN_NAMES = Object.keys(SCREENS) as (keyof typeof SCREENS)[];

export type RootStackParamList = {
Home: undefined;
NotFound: undefined;
} & {
[P in keyof typeof SCREENS]: typeof SCREENS[P]['params'];
};

// Make the default RootParamList the same as the RootStackParamList
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}

0 comments on commit 8850c51

Please sign in to comment.