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.

Navigation Parameters

As mentioned in the previous section, 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 * as React from "react"
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 properties for each component are properly typed. For this, we will create a type, ShopStackParamList.

For each screen we will type the expected properties 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 properties.

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 type { FC } from "react"
import { View, Text, Pressable } from "react-native"
import { useNavigation } from "@react-navigation/native"
import type { StackScreenProps } from "@react-navigation/stack"

import { ShopStackParamList } from "./StackRoute"

type ProfileProps = StackScreenProps<ShopStackParamList, "UserProfile">

const UserProfile: 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 properties 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 it's 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 type { FC } from "react"
import { Pressable, SafeAreaView } from "react-native"
import { NavigationContainer } from "@react-navigation/native"
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import Icon from "react-native-vector-icons/Ionicons"

import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import StateList from "./screens/StateList"
import Settings from "./screens/Settings"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import CityList from "./screens/CityList"
import Box from "./design/Box"
import Typography from "./design/Typography"
import { createStackNavigator } from "@react-navigation/stack"

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: 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()
export const AppNavigator: 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: FC = () => {
  return (
    <ThemeProvider>
      <SafeAreaView style={{ height: "100%", width: "100%" }}>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </SafeAreaView>
    </ThemeProvider>
  )
}

export default App

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

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

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

export interface State {
  name: string
  short: string
}

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

type Props = StackScreenProps<RestaurantsStackParamList, "StateList">

const StateList: FC = () => {
  const navigation = useNavigation()

  return (
    <Screen>
      <Card>
        <Typography variant="heading">
          Place My Order: Coming Soon To...
        </Typography>
      </Card>
      <FlatList
        data={states}
        renderItem={({ item: stateItem }) => (
          <Button
            onPress={() => {
              navigation.navigate("CityList")
            }}
          >
            {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"

describe("StateList", () => {
  it("renders states", async () => {
    render(
      <NavigationContainer>
        <StateList route={undefined} />
      </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.
  • Update the navigation.navigate to accept stateItem as a parameter.

Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

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

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

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

export interface State {
  name: string
  short: string
}

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

type Props = StackScreenProps<RestaurantsStackParamList, "StateList">

const StateList: FC<Props> = () => {
  const navigation = useNavigation()

  return (
    <Screen>
      <Card>
        <Typography variant="heading">
          Place My Order: Coming Soon To...
        </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

Setup 2

✏️ Update src/App.tsx to be:

import type { FC } from "react"
import { Pressable, SafeAreaView } from "react-native"
import { NavigationContainer } from "@react-navigation/native"
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import Icon from "react-native-vector-icons/Ionicons"

import ThemeProvider, { useTheme } from "./design/theme/ThemeProvider"
import StateList from "./screens/StateList"
import Settings from "./screens/Settings"
import RestaurantDetails from "./screens/RestaurantDetails"
import RestaurantList from "./screens/RestaurantList"
import CityList from "./screens/CityList"
import Box from "./design/Box"
import Typography from "./design/Typography"
import { createStackNavigator } from "@react-navigation/stack"

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: {
    state: {
      name: string
      short: string
    }
    city: {
      name: string
      state: string
    }
    slug: string
  }
}

const RestaurantsStack = createStackNavigator<RestaurantsStackParamList>()
const RestaurantsNavigator: 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()
export const AppNavigator: 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: FC = () => {
  return (
    <ThemeProvider>
      <SafeAreaView style={{ height: "100%", width: "100%" }}>
        <NavigationContainer>
          <AppNavigator />
        </NavigationContainer>
      </SafeAreaView>
    </ThemeProvider>
  )
}

export default App

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

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

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

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

type Props = StackScreenProps<RestaurantsStackParamList, "CityList">

const CityList: FC = () => {
  const navigation = useNavigation()

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

export default CityList

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

import type { StackScreenProps } from "@react-navigation/stack"
import type { RestaurantsStackParamList } from "../../App"
import type { FC } from "react"

import { FlatList } from "react-native"
import { useNavigation } from "@react-navigation/native"
import Box from "../../design/Box"
import Button from "../../design/Button"

type Props = StackScreenProps<RestaurantsStackParamList, "RestaurantList">

const restaurants = [
  {
    name: "Cheese Curd City",
    slug: "cheese-curd-city",
    images: {
      // thumbnail: CheeseThumbnail,
    },
    address: {
      street: "2451 W Washburne Ave",
      city: "Green Bay",
      state: "WI",
      zip: "53295",
    },
    _id: "Ar0qBJHxM3ecOhcr",
  },
  {
    name: "Poutine Palace",
    slug: "poutine-palace",
    images: {
      // thumbnail: PoutineThumbnail,
    },
    address: {
      street: "230 W Kinzie Street",
      city: "Green Bay",
      state: "WI",
      zip: "53205",
    },
    _id: "3ZOZyTY1LH26LnVw",
  },
]

const RestaurantList: FC<Props> = () => {
  const navigation = useNavigation()

  const navigateToDetails = () => {
    navigation.navigate("RestaurantDetails")
  }

  return (
    <>
      <Box padding="s">
        <FlatList
          data={restaurants}
          renderItem={({ item: restaurant }) => (
            <Button onPress={() => navigateToDetails(restaurant.slug)}>
              {restaurant.name}
            </Button>
          )}
          keyExtractor={(item) => item._id}
        />
      </Box>
    </>
  )
}

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"

describe("CityList component", () => {
  it("renders city List", () => {
    render(
      <NavigationContainer>
        <CityList
          route={{ params: { state: { name: "test", short: "test" } } }}
        />
      </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 params = {
  state: {
    short: "sT",
    name: "stateTest",
  },
  city: {
    name: "cityTest",
    state: "sT",
  },
  slug: "slugTest",
}

describe("RestaurantList component", () => {
  it("renders restaurant List", () => {
    render(
      <NavigationContainer>
        <RestaurantList route={{ params }} />
      </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 of for each component, to fetch its stored state.
  • Update the navigation.navigate to accept the necessary parameters.

Solution 2

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

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

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

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

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

type Props = StackScreenProps<RestaurantsStackParamList, "CityList">

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

  return (
    <Screen>
      <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 type { StackScreenProps } from "@react-navigation/stack"
import type { RestaurantsStackParamList } from "../../App"
import type { FC } from "react"

import { FlatList } from "react-native"
import { useNavigation } from "@react-navigation/native"
import Box from "../../design/Box"
import Button from "../../design/Button"

type Props = StackScreenProps<RestaurantsStackParamList, "RestaurantList">

const restaurants = [
  {
    name: "Cheese Curd City",
    slug: "cheese-curd-city",
    images: {
      // thumbnail: CheeseThumbnail,
    },
    address: {
      street: "2451 W Washburne Ave",
      city: "Green Bay",
      state: "WI",
      zip: "53295",
    },
    _id: "Ar0qBJHxM3ecOhcr",
  },
  {
    name: "Poutine Palace",
    slug: "poutine-palace",
    images: {
      // thumbnail: PoutineThumbnail,
    },
    address: {
      street: "230 W Kinzie Street",
      city: "Green Bay",
      state: "WI",
      zip: "53205",
    },
    _id: "3ZOZyTY1LH26LnVw",
  },
]

const RestaurantList: FC<Props> = ({ route }) => {
  const { state, city } = route.params
  const navigation = useNavigation()

  const navigateToDetails = (slug) => {
    navigation.navigate("RestaurantDetails", {
      state,
      city,
      slug,
    })
  }

  return (
    <>
      <Box padding="s">
        <FlatList
          data={restaurants}
          renderItem={({ item: restaurant }) => (
            <Button onPress={() => navigateToDetails(restaurant.slug)}>
              {restaurant.name}
            </Button>
          )}
          keyExtractor={(item) => item._id}
        />
      </Box>
    </>
  )
}

export default RestaurantList

Next steps

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