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 typeProps
made by theStackScreenProps
. - Update the
navigation.navigate
to acceptstateItem
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.