17

I'm trying to type the useNavigation from React Navigation. I would like to be able to pass only the name of the route, but I get an error unless I also pass props for that route.

Following the documentation, I understand the implementation should look something like this:

import { StackNavigationProp } from '@react-navigation/stack';

type StackParamList = {
    Home: { foo: string, onBar: () => void }
    About: AboutProps
}

type NavigationProps = StackNavigationProp<StackParamList>

const MyComponent = () => {
  const navigation = useNavigation<NavigationProps>()

  const handleOnNavigate = () => navigation.navigate('Home')
  //                                                    ^ TypeError!

I am geting a TypeError on the last line there. If I add the props for that route, the error disappears, but I shouldn't have to do that.

navigation.navigate('Home', { foo: 'hello', onBar: () => {} })

Looking at the type declaration for navigation.navigate (see below), this should not be necessary, as there is an overload for simply passing the name of the route as the only argument. I must be doing something wrong, since that is not accepted ... but what, where and why?

Here is a CodeSandBox reproducing the TypeError.

React Navigation types.d.ts (link)

navigate<RouteName extends keyof ParamList>(...args: undefined extends ParamList[RouteName]
  ? [screen: RouteName] | [screen: RouteName, params: ParamList[RouteName]]
  : [screen: RouteName, params: ParamList[RouteName]])
: void;
Nix
  • 5,746
  • 4
  • 30
  • 51

6 Answers6

6

When using Typescript the whole navigation should be strictly typed. Thankfully we don't need to write wrappers for their implementations, because we are supplied generic types.

The simplest way I found to add types to the useNavigation hook is the following:

The RootStackParamList type implementation needs to be altered if your route takes arguments

// src/_app.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

import HomeScreen from "./screens/home";
import AuthScreen from "./screens/auth";

export type ScreenNames = ["Home", "Auth"] // type these manually
export type RootStackParamList = Record<ScreenNames[number], undefined>;
export type StackNavigation = NavigationProp<RootStackParamList>;

const Stack = createNativeStackNavigator<RootStackParamList>();

export const App = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Auth" component={AuthScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
// src/screens/home.tsx
import { useNavigation } from '@react-navigation/native';
import { type StackNavigation } from "../_app";

const HomeScreen = () => {
  const { navigate } = useNavigation<StackNavigation>();

  const handleOnNavigate = () => navigate("Home");

  // ... the rest of the component code
}

export default HomeScreen;

Also note that they don't use the useNavigation hook for pages/screens, rather deeply nested components, since the navigator already gets passed as a prop to the components passed into the Stack.Screen, meaning you can also use the HomeScreen's navigator the following way:

// src/screens/home.tsx
import { type StackNavigation } from "../_app";

interface HomeScreenProps {
  navigation: StackNavigation;
}

const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
  const handleOnNavigate = () => navigation.navigate("Home");

  // ... the rest of the component code
}

export default HomeScreen;
Dirk Beukes
  • 61
  • 1
  • 1
4

In order to make it work with only one argument you need to add | undefined.

Let me explain why you need to do it.

Consider this declareation:

navigate<RouteName extends keyof ParamList>(
    ...args: undefined extends ParamList[RouteName]
      ? [screen: RouteName] | [screen: RouteName, params: ParamList[RouteName]]
      : [screen: RouteName, params: ParamList[RouteName]]
  ): void;

This is the most interesting part: undefined extends ParamList[RouteName] It means that if undefined extends ParamList[RouteName] you are allowed to use only one argument.

Let's split it into smaller examples:

type ParamList = {
  Home: { foo: string; onBar: () => void };
  About: { bar: string; onBaz: () => void };
  Undefined: undefined,
};

type Check<T extends keyof ParamList> = undefined extends ParamList[T] ? 'might be undefined' : 'can not be undefined'

type Result = Check<'Home'> // "can not be undefined"
type Result2 = Check<'Undefined'> // "might be undefined"

As you might have noticed, if you provide Home TS will require two arguments because ParamList['Home'] returns an object which can not be undefined.

On the other hand, undefined extends ParamList['Undefined'] - hence TS allows you to use only one argument.

That's why TS does not allow you to pass only one argument.

Dharman
  • 30,962
  • 25
  • 85
  • 135
  • 1
    Thanks for providing this answer, and for walking through the type declaration - and also for helping me improve the question itself. +1! – Nix Aug 11 '21 at 12:36
3

I am geting a TypeError on the last line there. If I add the props for that route, the error disappears, but I shouldn't have to do that.

You specified this in types:

Home: { foo: string, onBar: () => void }

Which means Home takes these params. If your route doesn't take any params and you could just do navigate('Home'), you shouldn't be specifying any params in the types.

If those params are optional, then you need to specify the type accordingly:

Home: { foo: string, onBar: () => void } | undefined
satya164
  • 9,464
  • 2
  • 31
  • 42
  • Thank you, @satya164. I am new to React Navigation, but it seems to work differently from routers targeting the web browser. The component does have props, but they are defined and passed to the component in a parent component. So if I understand correctly, if a screen needs props, I always need to pass them directly from wherever I am navigating? I should note, that my current implementation works perfectly - it just reports a type error. – Nix Aug 11 '21 at 11:16
  • 1
    They are not any props, they are params. Params are used for passing data **when navigating**. You're using `initialParams`, which is useful for home screen where it'll renders without navigating, but generally that's not the primary use case for params. If you want to just pass some props, then pass it like this: https://reactnavigation.org/docs/hello-react-navigation/#passing-additional-props – satya164 Aug 11 '21 at 11:49
  • Aha! There's my confusion; for some reason I thought `initialParams` was the idiomatic way to pass props in the Stack Navigator. I've replaced it with render props, and now it both works as expected with no type errors. – Nix Aug 11 '21 at 12:30
3

Within your type StackParamList you define what kind of params you expect when navigating to your screens. If you don't expect any params for Home, you also have to define it in your type accordingly:

export type StackParamList = {
  Home: undefined,
  About: { bar: string; onBaz: () => void };
};

If you expect any additional params optionally, you can use the pipe in your type declaration:

export type StackParamList = {
  Home: { foo: string; onBar: () => void } | undefined;
  About: { bar: string; onBaz: () => void };
};

// Will work in both ways: 

const handleOnNavigate = () => navigation.navigate('Home')

As a workaround you can also use another overload and pass a configuration object:

const handleOnNavigate = () => navigation.navigate({key : "Home"});
Chris
  • 4,403
  • 4
  • 42
  • 54
  • 1
    Thank you for providing this answer. It is correct, but I have accepted @satya164's answer, as they answer were first. – Nix Aug 11 '21 at 12:34
1
import { useNavigation, ParamListBase,  NavigationProp } from '@react-navigation/native';
const navigation: NavigationProp<ParamListBase> = useNavigation();

all you need to do is imported required hook, prop and type and simply apply like above showed in example Cheers

Dilnawaz Khan
  • 164
  • 1
  • 4
0
import { StackNavigationProp } from '@react-navigation/stack';

type StackParamList = {
    Home: { foo: string, onBar: () => void }
    About: AboutProps
}

// type NavigationProps = StackNavigationProp<StackParamList>
type NavigationProps = StackNavigationProp<StackParamList, 'Home'>

const MyComponent = () => {
  const navigation = useNavigation<NavigationProps>()

  const handleOnNavigate = () => navigation.navigate('Home')

See the commented out NavigationProps and what it should probably be below it.

Templar
  • 11
  • 2