How to structure React Native navigation for property passing with Typescript

Viewed 354

I have a simple React Native app I'm putting together using Typescript and I'm trying to understand how to make the navigation work correctly.

I a list of content sections and when one is selected to open the chosen article in a webview. What I can't grasp is how I am supposed to pass the URL for the webview to load to the view that contains it. Both the navigation list and the web view should fill the screen when they are selected.

As things stand my base component is GuidanceNavigation which contains a Stack.Navigator with a Stack.Screen for the NavigationItems view that lists all the topics and a Stack.Screen for the InstructionView that contains the webview to show the instructions.

As things stand these are all React.Component types and the part I am not clear on is that when I am in the NavigationItems view I select an option and I need the parent view to replace the NavigationItems view with InstructionView, providing the InstructionView with the title and file location for the selected article. As far as I can tell React Navigation does some magic involving passing in some navigation properties, but I can't figure out how that is supposed to work in a Typescript context or how I can pass my typed objects through the Stack Navigator or even whether the Stack Navigator is the correct component to use in this situation. The Typescript-specific documentation talks about passing through navigation properties but doesn't explain how this is related to the navigator object that seems to be available in JavaScript to actually perform navigation.

My code looks like this, it's a while since I have worked with React and my first time using react-native so it's probably neither excellent nor idiomatic:

GuidanceNavigation.tsx:

const Stack = createStackNavigator();

export class GuidanceNavigation extends React.Component<{}, {}>  {

  render(): JSX.Element {
    return (
      <SafeAreaView style={styles.container} >
        <NavigationContainer>
          <Stack.Navigator>
            <Stack.Screen name="Choose a topic" component="NavigationItems" />
            <Stack.Screen name="Instructions" component="InstructionView" />
      </Stack.Navigator>
      </NavigationContainer>
      </SafeAreaView>
    );
  };

}

NavigationTypes.ts:

export interface IFileLink {
    title: string,
    file: string
}

NavigationItems.tsx:

import { IFileLink } from './NavigationTypes';

export class NavigationItems extends React.Component<{}, {}>  {

  itemId: number = 0;
  
  readonly items = [
    {
      section: R.strings.menu_steps,
      values: [
    { title: R.strings.menu_getting_started, file: "./assets/html/getting-started.html" },
     // and so on

      ]
    },
    {
      section: R.strings.menu_guidance,
      values: [
    { title: R.strings.menu_glossary, file: "./assets/html/glossary.html" },
        // etc
      ]
    }
  ];

  itemList(itemList: Array<IFileLink>): JSX.Element[] {
    return itemList.map((it) => this.item(it));
  };

  navigateToFile(link:IFileLink) {
    console.log("Navigation to "+link.title);
    // This does not work because navigation comes from ??? somewhere???
    navigation.navigate('Instructions', link);
  };

  item(fileLink:IFileLink): JSX.Element {
    return(
      <Button
    key= { this.itemId++ }
    title= { fileLink.title }
    onPress= {() =>
      this.navigateToFile(fileLink)
    }
      />
    );
  };

  render(): JSX.Element {
    return (
      <ScrollView>
          <Text style={styles.sectionTitle}>{this.items[0].section}</Text>
          {this.itemList(this.items[0].values)}
          <Text style={styles.sectionTitle}>{this.items[1].section}</Text>
          {this.itemList(this.items[1].values)}
      </ScrollView>
    )
  };

}

InstructionView.tsx:

import { IFileLink } from './NavigationTypes';

export class InstructionView extends React.Component<IFileLink> {

  private fileLink: IFileLink;

  constructor(fl: IFileLink) {
    super(fl);
    this.fileLink = fl;
  }

  render(): JSX.Element {
    return (
        <WebView
          source={{ uri: this.fileLink.file }}
          />
    );
  }
}

How can I ensure that I navigate from NavigationItems to InstructionView with the required properties when an item is selected in that list?

1 Answers

This does not work because navigation comes from ??? somewhere???

The navigation object comes from the props of the component. When your component is loaded through Stack.Screen, react-navigation calls the component with props route and navigation.

On the typescript side of things, you need to declare that the NavigationItems component must be called with this navigation prop. How you do that is what you are seeing explained in the docs.

You declare a RootStackParamList type that maps your screen names to their required props.

type RootStackParamList = {
    ['Choose a topic']: undefined; // no required props
    Instructions: IFileLink; // requires an IFileLink as props
};

You use this type as the generic when you create the Stack instance.

const Stack = createStackNavigator<RootStackParamList>();

You use this type again as the generic when you declare the type of the navigation prop for the NavigationItems component. Now your component know knows that it will have a prop navigate, so you can call this.props.navigate. It also knows very detailed information about where you can navigate to. The screen name must be either 'Choose a topic' or 'Instructions' or else you'll get an error. It also knows about all of the screens' unique prop requirements, so you now get an error if you try to navigate to Instructions without passing an IFileLink as the second argument.

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

type ItemsProps = {
    navigation: StackNavigationProp<RootStackParamList, 'Choose a topic'>;
};

export class NavigationItems extends React.Component<ItemsProps, {}>  {

    navigateToFile(link: IFileLink) {
        console.log("Navigation to " + link.title);
        // can now navigate via props
        this.props.navigation.navigate('Instructions', link);
    };

    /*...*/
}

There is an error inside your Navigator. You need to pass the components themselves, not their string names.

<Stack.Screen name="Choose a topic" component={NavigationItems} />
<Stack.Screen name="Instructions" component={InstructionView} />

Typescript Playground Link

Related