Storing State in Navigation Parameters page

Maintain the React state with React Native Navigation Parameters

Overview

In this section, you will:

  • Strongly type the navigation parameters of an application.
  • Maintain and pass the state using route params through navigation.

Objective 1: Intro to navigation parameters

Now that we’ve successfully implemented React Navigation in our application, we can navigate between screens easily. The only remaining issue is that we lack away to pass information, or state, between screens. So, our new goal for this section is passing state between screens using navigation parameters.

If you’re familiar with web development, you might know the best practice to update the URL with parameters that reflect the state of the application. For Single Page Applications (SPAs), this includes updating the URL with the current navigation state.

Since our React Native application isn’t navigated through URLs, we aren’t able to pass the parameters through a URL. Instead, we’ll be using the Stack we’ve already made.

import { createStackNavigator } from "@react-navigation/stack"

export type ShopStackParamList = {
  Home: undefined
  UserProfile: {
    user: {
      firstName: string
      lastName: string
      email: string
    }
    theme: "dark" | "light"
  }
  Storefront: {
    user: {
      firstName: string
      lastName: string
      email: string
    }
    slug: string
    favorites: string[]
  }
}

const ShoppingStack = createStackNavigator<ShopStackParamList>()

const ShopApp = () => {
  return (
    <ShoppingStack.Navigator
      initialRouteName="Home"
      screenOptions={{
        headerMode: "screen",
        headerTintColor: "white",
        headerStyle: { backgroundColor: "tomato" },
      }}
    >
      <ShoppingStack.Screen name="Home" component={Home} />
      <ShoppingStack.Screen name="UserProfile" component={UserProfile} />
      <ShoppingStack.Screen name="Storefront" component={Storefront} />
    </ShoppingStack.Navigator>
  )
}

Before we get into using route on each Screen of the Navigator, considering we're using TypeScript, we need to make an effort to make sure the props for each component are properly typed. For this, we will create a type, ShopStackParamList.

For each screen we will type the expected props that will be passed along each route. The Home in this case doesn’t expect any parameters to be passed to it, so we leave it undefined. The UserProfile and Storefront contain a few props.

Now, our createStackNavigator includes a type we’ve made ShopStackParamList. Because of this, now if we provide our screen components Props as route params, TypeScript will be able to able to identify what parameters are accessible from the components route.params.

While the route is accessible from the Navigator, it is also accessible from the component that is being navigated to through props.

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { View, Text, Pressable } from "react-native"

import { ShopStackParamList } from "./StackRoute"

type ProfileProps = StackScreenProps<ShopStackParamList, "UserProfile">

const UserProfile: React.FC<ProfileProps> = ({ route }) => {
  const { user } = route.params
  const navigation = useNavigation()

  return (
    <View>
      <Text variant="heading">
        Hello! {user.firstName} {user.lastName}. Is your {user.email} correct?
      </Text>
      <Pressable
        onPress={() => {
          navigation.navigate("Storefront", { user, slug: "mainPage" })
        }}
      >
        Shop Here!
      </Pressable>
    </View>
  )
}

To make sure the Props for our component match up to what we have for our StackNavigator, we can import the type we made and reference the UserProfile props specifically.

As you can see, in the UserProfile component, we can access the route.params of the component if any are provided. We grab the user, and are able to use its properties throughout the component.

This includes passing the state of user through navigation. We can add user, and other properties as an object for the second argument of navigation.navigate. Thus on the Storefront screen, all of those params passed will be accessible within its component.

Setup 1

✏️ Update src/App.tsx to be:

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack"
import { Pressable } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import Icon from "react-native-vector-icons/Ionicons"

import Box from "./design/Box"
import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import Typography from "./design/Typography"
import CityList from "./screens/CityList"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import Settings from "./screens/Settings"
import StateList from "./screens/StateList"

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

export type RestaurantsStackParamList = {
  StateList: undefined
  CityList: {
    state: {
      name: string
      short: string
    }
  }
}

const RestaurantsStack = createStackNavigator<RestaurantsStackParamList>()
const RestaurantsNavigator: React.FC = () => {
  return (
    <RestaurantsStack.Navigator
      initialRouteName="StateList"
      screenOptions={{
        header: ({ route, navigation }) => {
          if (!navigation.canGoBack()) return null

          return (
            <Pressable onPress={navigation.goBack}>
              <Box
                padding="m"
                style={{ flexDirection: "row", gap: 8, alignItems: "center" }}
              >
                <Icon name="arrow-back" size={20} />
                <Typography variant="heading">
                  {/* @ts-ignore */}
                  {[route.params?.city?.name, route.params?.state?.name]
                    .filter(Boolean)
                    .join(", ")}
                </Typography>
              </Box>
            </Pressable>
          )
        },
      }}
    >
      <RestaurantsStack.Screen name="StateList" component={StateList} />
      <RestaurantsStack.Screen name="CityList" component={CityList} />
      <RestaurantsStack.Screen
        // @ts-ignore: We will fix this in the next exercise
        name="RestaurantList"
        component={RestaurantList}
      />
      <RestaurantsStack.Screen
        // @ts-ignore: We will fix this in the next exercise
        name="RestaurantDetails"
        component={RestaurantDetails}
      />
    </RestaurantsStack.Navigator>
  )
}

const AppTabs = createBottomTabNavigator()
const AppNavigator: React.FC = () => {
  const theme = useTheme()

  return (
    <AppTabs.Navigator
      initialRouteName="RestaurantsStack"
      screenOptions={({ route }) => ({
        headerStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        headerTitleStyle: {
          color: theme.palette.screen.contrast,
          ...theme.typography.title,
        },
        tabBarStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        tabBarActiveTintColor: theme.palette.primary.strong,
        tabBarInactiveTintColor: theme.palette.screen.contrast,
        tabBarIcon: ({ focused, color }) => {
          let icon = "settings"
          if (route.name === "Settings") {
            icon = focused ? "settings" : "settings-outline"
          } else if (route.name === "Restaurants") {
            icon = focused ? "restaurant" : "restaurant-outline"
          }

          return <Icon name={icon} size={20} color={color} />
        },
      })}
    >
      <AppTabs.Screen
        name="Restaurants"
        component={RestaurantsNavigator}
        options={{ title: "Place My Order" }}
      />
      <AppTabs.Screen
        name="Settings"
        component={Settings}
        options={{ title: "Settings" }}
      />
    </AppTabs.Navigator>
  )
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ThemeProvider>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </ThemeProvider>
    </SafeAreaView>
  )
}

export default App

✏️ Update src/screens/StateList/StateList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

export interface StateListProps
  extends StackScreenProps<RestaurantsStackParamList, "StateList"> {}

// Exercise: Update the typing of `StateList` component, using the type `Props` made by the `StackScreenProps`.
const StateList: React.FC = () => {
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <Card>
        <Typography variant="heading">Place My Order: Coming Soon!</Typography>
      </Card>
      <FlatList
        data={states}
        renderItem={({ item: stateItem }) => (
          <Button
            onPress={() => {
              // Exercise: Navigate to the CityList view.
            }}
          >
            {stateItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.short}
      />
    </Screen>
  )
}

export default StateList

Verify 1

✏️ Update src/screens/StateList/StateList.test.tsx to be:

import { NavigationContainer } from "@react-navigation/native"
import { render, screen } from "@testing-library/react-native"

import StateList from "./StateList"

const route = {
  key: "StateList",
  name: "StateList",
  params: undefined,
} as const

describe("Screens/StateList", () => {
  it("renders", () => {
    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <StateList route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

Exercise 1

  • Update the typing of StateList component, using the type Props made by the StackScreenProps.
  • Navigate to the CityList view.

Solution 1

Note: The tests will pass before you make any changes, so use the application in the emulator to verify your solution!

Click to see the solution

✏️ Update src/screens/StateList/StateList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

export interface StateListProps
  extends StackScreenProps<RestaurantsStackParamList, "StateList"> {}

const StateList: React.FC<StateListProps> = () => {
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <Card>
        <Typography variant="heading">Place My Order: Coming Soon!</Typography>
      </Card>
      <FlatList
        data={states}
        renderItem={({ item: stateItem }) => (
          <Button
            onPress={() => {
              navigation.navigate("CityList", { state: stateItem })
            }}
          >
            {stateItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.short}
      />
    </Screen>
  )
}

export default StateList

Objective 2: Implement city and restaurant params

Now let’s implement the CityList and RestaurantList params!

Setup 2

✏️ Update src/App.tsx to be:

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack"
import { Pressable } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import Icon from "react-native-vector-icons/Ionicons"

import Box from "./design/Box"
import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import Typography from "./design/Typography"
import CityList from "./screens/CityList"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import Settings from "./screens/Settings"
import StateList from "./screens/StateList"

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

export type RestaurantsStackParamList = {
  StateList: undefined
  CityList: {
    state: {
      name: string
      short: string
    }
  }
  RestaurantList: {
    state: {
      name: string
      short: string
    }
    city: {
      name: string
      state: string
    }
  }
  RestaurantDetails: {
    slug: string
  }
}

const RestaurantsStack = createStackNavigator<RestaurantsStackParamList>()
const RestaurantsNavigator: React.FC = () => {
  return (
    <RestaurantsStack.Navigator
      initialRouteName="StateList"
      screenOptions={{
        header: ({ route, navigation }) => {
          if (!navigation.canGoBack()) return null

          return (
            <Pressable onPress={navigation.goBack}>
              <Box
                padding="m"
                style={{ flexDirection: "row", gap: 8, alignItems: "center" }}
              >
                <Icon name="arrow-back" size={20} />
                <Typography variant="heading">
                  {/* @ts-ignore */}
                  {[route.params?.city?.name, route.params?.state?.name]
                    .filter(Boolean)
                    .join(", ")}
                </Typography>
              </Box>
            </Pressable>
          )
        },
      }}
    >
      <RestaurantsStack.Screen name="StateList" component={StateList} />
      <RestaurantsStack.Screen name="CityList" component={CityList} />
      <RestaurantsStack.Screen
        name="RestaurantList"
        component={RestaurantList}
      />
      <RestaurantsStack.Screen
        name="RestaurantDetails"
        component={RestaurantDetails}
      />
    </RestaurantsStack.Navigator>
  )
}

const AppTabs = createBottomTabNavigator()
const AppNavigator: React.FC = () => {
  const theme = useTheme()

  return (
    <AppTabs.Navigator
      initialRouteName="RestaurantsStack"
      screenOptions={({ route }) => ({
        headerStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        headerTitleStyle: {
          color: theme.palette.screen.contrast,
          ...theme.typography.title,
        },
        tabBarStyle: {
          backgroundColor: theme.palette.screen.main,
        },
        tabBarActiveTintColor: theme.palette.primary.strong,
        tabBarInactiveTintColor: theme.palette.screen.contrast,
        tabBarIcon: ({ focused, color }) => {
          let icon = "settings"
          if (route.name === "Settings") {
            icon = focused ? "settings" : "settings-outline"
          } else if (route.name === "Restaurants") {
            icon = focused ? "restaurant" : "restaurant-outline"
          }

          return <Icon name={icon} size={20} color={color} />
        },
      })}
    >
      <AppTabs.Screen
        name="Restaurants"
        component={RestaurantsNavigator}
        options={{ title: "Place My Order" }}
      />
      <AppTabs.Screen
        name="Settings"
        component={Settings}
        options={{ title: "Settings" }}
      />
    </AppTabs.Navigator>
  )
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ThemeProvider>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </ThemeProvider>
    </SafeAreaView>
  )
}

export default App

✏️ Update src/screens/CityList/CityList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Screen from "../../design/Screen"

const cities = [
  { name: "Madison", state: "WI" },
  { name: "Springfield", state: "IL" },
]

export interface CityListProps
  extends StackScreenProps<RestaurantsStackParamList, "CityList"> {}

// Exercise: Update the the typing to use the given `Props`.
const CityList: React.FC = () => {
  // Exercise: Destructure the `route` to fetch its stored state.
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <FlatList
        data={cities}
        renderItem={({ item: cityItem }) => (
          <Button
            onPress={() => {
              // Exercise: Navigate to the RestaurantList view.
            }}
          >
            {cityItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.name}
      />
    </Screen>
  )
}

export default CityList

✏️ Update src/screens/RestaurantList/RestaurantList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Screen from "../../design/Screen"

export interface RestaurantListProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}

const restaurants = [
  {
    name: "Cheese Curd City",
    slug: "cheese-curd-city",
    images: {
      thumbnail:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-thumbnail.jpg",
      owner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-owner.jpg",
      banner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/1-banner.jpg",
    },
    address: {
      street: "2451 W Washburne Ave",
      city: "Green Bay",
      state: "WI",
      zip: "53295",
    },
    _id: "Ar0qBJHxM3ecOhcr",
  },
  {
    name: "Poutine Palace",
    slug: "poutine-palace",
    images: {
      thumbnail:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-thumbnail.jpg",
      owner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-owner.jpg",
      banner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/1-banner.jpg",
    },
    address: {
      street: "230 W Kinzie Street",
      city: "Green Bay",
      state: "WI",
      zip: "53205",
    },
    _id: "3ZOZyTY1LH26LnVw",
  },
]

// Exercise: Update the the typing to use the given `Props`.
const RestaurantList: React.FC<RestaurantListProps> = () => {
  // Exercise: Destructure the `route` to fetch its stored state.
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <FlatList
        data={restaurants}
        renderItem={({ item: restaurant }) => (
          <Button
            onPress={() => {
              // Exercise: Navigate to the RestaurantDetails view.
            }}
          >
            {restaurant.name}
          </Button>
        )}
        keyExtractor={(item) => item._id}
      />
    </Screen>
  )
}

export default RestaurantList

Verify 2

✏️ Update src/screens/CityList/CityList.test.tsx to be:

import { NavigationContainer } from "@react-navigation/native"
import { render, screen } from "@testing-library/react-native"

import CityList from "./CityList"

const route = {
  key: "CityList",
  name: "CityList",
  params: {
    state: {
      name: "name",
      short: "short",
    },
  },
} as const

describe("Screens/CityList", () => {
  it("renders", () => {
    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <CityList route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Madison/i)).toBeOnTheScreen()
    expect(screen.getByText(/Springfield/i)).toBeOnTheScreen()
  })
})

✏️ Update src/screens/RestaurantList/RestaurantList.test.tsx to be:

import { NavigationContainer } from "@react-navigation/native"
import { render, screen } from "@testing-library/react-native"

import RestaurantList from "./RestaurantList"

const route = {
  key: "RestaurantList",
  name: "RestaurantList",
  params: {
    state: {
      name: "name",
      short: "short",
    },
    city: {
      name: "name",
      state: "state",
    },
    slug: "test",
  },
} as const

describe("Screens/RestaurantList", () => {
  it("renders", () => {
    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantList route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Cheese Curd City/i)).toBeOnTheScreen()
    expect(screen.getByText(/Poutine Palace/i)).toBeOnTheScreen()
  })
})

Exercise 2

For both the CityList and RestaurantList components:

  • Update the the typing of each component to use the given Props.
  • Destructure the route for each component, to fetch its stored state.
  • Update the navigation.navigate to accept the necessary parameters.

Solution 2

Note: The tests will pass before you make any changes, so use the application in the emulator to verify your solution!

Click to see the solution

✏️ Update src/screens/CityList/CityList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Screen from "../../design/Screen"

const cities = [
  { name: "Madison", state: "WI" },
  { name: "Springfield", state: "IL" },
]

export interface CityListProps
  extends StackScreenProps<RestaurantsStackParamList, "CityList"> {}

const CityList: React.FC<CityListProps> = ({ route }) => {
  const { state } = route.params
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <FlatList
        data={cities}
        renderItem={({ item: cityItem }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantList", {
                state,
                city: cityItem,
              })
            }}
          >
            {cityItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.name}
      />
    </Screen>
  )
}

export default CityList

✏️ Update src/screens/RestaurantList/RestaurantList.tsx to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"
import Button from "../../design/Button"
import Screen from "../../design/Screen"

export interface RestaurantListProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}

const restaurants = [
  {
    name: "Cheese Curd City",
    slug: "cheese-curd-city",
    images: {
      thumbnail:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-thumbnail.jpg",
      owner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-owner.jpg",
      banner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/1-banner.jpg",
    },
    address: {
      street: "2451 W Washburne Ave",
      city: "Green Bay",
      state: "WI",
      zip: "53295",
    },
    _id: "Ar0qBJHxM3ecOhcr",
  },
  {
    name: "Poutine Palace",
    slug: "poutine-palace",
    images: {
      thumbnail:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-thumbnail.jpg",
      owner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/2-owner.jpg",
      banner:
        "https://www.place-my-order.com/node_modules/place-my-order-assets/images/1-banner.jpg",
    },
    address: {
      street: "230 W Kinzie Street",
      city: "Green Bay",
      state: "WI",
      zip: "53205",
    },
    _id: "3ZOZyTY1LH26LnVw",
  },
]

const RestaurantList: React.FC<RestaurantListProps> = ({ route }) => {
  const navigation = useNavigation()

  return (
    <Screen noScroll>
      <FlatList
        data={restaurants}
        renderItem={({ item: restaurant }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantDetails", {
                slug: restaurant.slug,
              })
            }}
          >
            {restaurant.name}
          </Button>
        )}
        keyExtractor={(item) => item._id}
      />
    </Screen>
  )
}

export default RestaurantList

Next steps

Next, we’ll cover an essential part of nearly all web applications: Making HTTP Requests.