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
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 typeProps
made by theStackScreenProps
. - 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.