Handling User Inputs page

Use switch controls and inputs to collect information.

Overview

In this section, you will:

  • Handle checkbox-like behavior with <Switch>.
  • Use TypeScript’s Record interface.
  • Set state using an updater function.
  • Update reference types and rendering.
  • Use the TextInput component.
  • Label text inputs.
  • Create unique IDs with the useId() Hook.

Objective 1: Add switch controls to select menu items

Now let’s create a RestaurantOrder view for users to start selecting menu items!

We’ll start with switch controls to select menu items, with a message that warns users when no items are selected and a total that shows the sum of the selected items.

Screenshot of the top of the application restaurant order page. Screenshot of the bottom of the application restaurant order page.

Handling checkbox-like behavior with <Switch>

For boolean values, such as toggles, React Native provides the Switch component. You handle changes using the onValueChange prop, which directly provides the new boolean value.

[If you’re familiar with web development, React Native does not have a direct equivalent to the HTML <input type="checkbox">. Instead, you use the <Switch> component to achieve similar functionality.]

Let’s take a look at an example using <Switch>:

import { useState } from "react"
import { Switch, Text, View } from "react-native"

const SwitchExample = () => {
  const [isEnabled, setIsEnabled] = useState(false)

  return (
    <View>
      <Switch
        onValueChange={(newValue) => setIsEnabled(newValue)}
        thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
        trackColor={{ false: "#767577", true: "#81b0ff" }}
        value={isEnabled}
      />
      <Text>{isEnabled ? "Enabled" : "Disabled"}</Text>
    </View>
  )
}

export default SwitchExample

In the example above, you can see the value passed in is the isEnabled state.

Then, the onValueChange prop takes a function that will be called with the new value when the switch is toggled. This is usually what you need, although there is an onChange prop if you need your function to be called with an event object instead.

Using TypeScript’s Record interface

In our upcoming exercise, we want to store information in a JavaScript object. We also want to use TypeScript so we can constrain the types used as keys and values.

TypeScript provides a handy interface named Record that we can use. Record is a generic interface that requires two types: the first is the type of the keys, and the second is the type of the values.

For example, if we’re recording the items in a list that are selected, we might capture the item’s name and whether or not it’s selected like this:

import { useState } from "react"
import { View, Text, Switch } from "react-native"

const landmarks = [
  { id: "0b90c705", name: "Eiffel Tower" },
  { id: "5be758c1", name: "Machu Picchu" },
  { id: "206025c3", name: "Taj Mahal" },
]

type SelectedItems = Record<string, boolean>

const Selected = () => {
  const [selected, setSelected] = useState<SelectedItems>({})

  function handleChange(name: string, isSelected: boolean) {
    setSelected((current) => ({ ...current, [name]: isSelected }))
  }

  return (
    <View>
      {landmarks.map((landmark) => {
        return (
          <View
            key={landmark.id}
            style={{
              flexDirection: "row",
              alignItems: "center",
              marginBottom: 10,
            }}
          >
            <Text>{landmark.name}: </Text>
            <Switch
              onValueChange={(newValue) =>
                handleChange(landmark.name, newValue)
              }
              value={!!selected[landmark.name]}
            />
          </View>
        )
      })}
    </View>
  )
}

export default Selected

We’ve explicitly defined the type of useState as a Record<string, boolean>; all the keys must be strings, and all the values must be booleans. Fortunately, JavaScript’s object implements the Record interface, so we can set the default value to an empty object instance.

Now let’s see how we can use a Record to store state data.

Setting state using an updater function

One challenge we face when using an object for state is that we probably need to merge the current state value with the new state value. Why? Imagine we have a state object that already has multiple keys and values, and we need to add a new key and value.

Well, we’re in luck! React already has a solution for this: the setter function returned by useState will accept an “updater function” that’s passed the “current” state value and should return the “next” state value.

import { useState } from "react"
import { View, Text, Switch } from "react-native"

const landmarks = [
  { id: "0b90c705", name: "Eiffel Tower" },
  { id: "5be758c1", name: "Machu Picchu" },
  { id: "206025c3", name: "Taj Mahal" },
]

type SelectedItems = Record<string, boolean>

const Selected: React.FC = () => {
  const [selected, setSelected] = useState<SelectedItems>({})

  function handleSelectedChange(name: string, isSelected: boolean) {
    setSelected((currentSelectedItems) => {
      const updatedSelectedItems = {
        ...currentSelectedItems,
      }

      if (isSelected) {
        updatedSelectedItems[name] = true
      } else {
        delete updatedSelectedItems[name]
      }

      return updatedSelectedItems
    })
  }

  return (
    <View>
      {landmarks.map((landmark) => (
        <View
          key={landmark.id}
          style={{
            flexDirection: "row",
            alignItems: "center",
            marginBottom: 10,
          }}
        >
          <Text>{landmark.name}: </Text>
          <Switch
            onValueChange={(newValue) =>
              handleSelectedChange(landmark.name, newValue)
            }
            value={!!selected[landmark.name]}
          />
        </View>
      ))}
    </View>
  )
}

export default Selected

In the example above, the onChange event handler calls handleSelectedChange, which accepts a name string and a boolean.

In turn, handleSelectedChange calls setSelected with an updater function as the argument. The updater function accepts the currentSelectedItems argument, which is the object with the currently-selected items before our switch was toggled.

We will dig into how we create the updatedSelectedItems object in just a bit, but for now let’s take note that we create a new updatedSelectedItems object and return it from our updater function. This gives React the updated selected state and allows React to re-render the component.

Updating reference types and rendering

Now let’s explain how the updater function works in the example above. The updater function does not mutate the current object, then return it; instead, it makes a new object and populates it with the contents of the current object.

This is an important detail because, after the updater function runs, React will compare the values of the current and next objects to determine if they are different. If they are different, React will re-render the Selected component; if they are the same, then React will do nothing.

The same rules apply when state is an array: create a new array, then update the contents of the new array.

Here’s how to add an item when state (current) is an array:

setSelectedOrders((current) => {
  const next = [...current, newOrder]
  return next
})

Here’s how to replace an item when state (current) is an array:

setUpdatedRestaurant((current) => {
  const next = [
    ...current.filter((item) => item.id !== updatedRestaurant.id),
    updatedRestaurant,
  ]

  return next
})

OK, that was a lot. Let’s start making some code changes so we can select menu items for an order.

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 RestaurantOrder from "./screens/RestaurantOrder"
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
  }
  RestaurantOrder: {
    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}
      />
      {/* Exercise: Add RestaurantOrder page to the stack. */}
    </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/RestaurantDetails/RestaurantDetails.tsx to be:

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

import { RestaurantsStackParamList } from "../../App"
import Loading from "../../components/Loading"
import RestaurantHeader from "../../components/RestaurantHeader"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantDetailsProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantDetails"> {}

const RestaurantDetails: React.FC<RestaurantDetailsProps> = ({ route }) => {
  const { slug } = route.params
  const navigation = useNavigation()
  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `${restaurant.name}` })
    }
  }, [restaurant, navigation])

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant details:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen>
      <RestaurantHeader restaurant={restaurant} />
      <Button onPress={() => console.warn("Place an order")}>
        Place an order
      </Button>
      {/* Exercise: Add Button that links to RestaurantOrder page. */}
    </Screen>
  )
}

export default RestaurantDetails

✏️ Create src/screens/RestaurantOrder/RestaurantOrder.tsx and update it to be:

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { useEffect, useState } from "react"

import { RestaurantsStackParamList } from "../../App"
import FormSwitch from "../../components/FormSwitch"
import Loading from "../../components/Loading"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantOrderProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantOrder"> {}

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC<RestaurantOrderProps> = ({ route }) => {
  const navigation = useNavigation()
  const { slug } = route.params

  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `Order from ${restaurant.name}` })
    }
  }, [restaurant, navigation])

  const handleOrder = () => {
    // eslint-disable-next-line no-console
    console.info("“Place My Order” button pressed!")
  }

  const selectedCount = Object.values(items).length
  const subtotal = 0 // Exercise: Use calculateTotal here.

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant order:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  if (!restaurant) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Restaurant not found</Typography>
        </Box>
      </Screen>
    )
  }

  return (
    <Screen>
      <Card title="Lunch Menu">
        {/* Exercise: List food items with checkboxes. */}
      </Card>

      <Card title="Dinner Menu">
        {/* Exercise: List food items with checkboxes. */}
      </Card>

      <Card title="Order Details"></Card>

      <Box padding="s">
        {subtotal === 0 ? (
          <Typography>Please choose an item.</Typography>
        ) : (
          <Typography>{selectedCount} items selected.</Typography>
        )}
      </Box>

      <Box padding="s">
        <Typography variant="heading">Total: ${subtotal.toFixed(2)}</Typography>
      </Box>

      <Box padding="s">
        <Button onPress={handleOrder}>Place My Order!</Button>
      </Box>
    </Screen>
  )
}

function calculateTotal(items: OrderItems) {
  return Object.values(items).reduce((total, itemPrice) => {
    return total + itemPrice
  }, 0)
}

export default RestaurantOrder

✏️ Create src/screens/RestaurantOrder/index.ts and update it to be:

export { default } from "./RestaurantOrder"
export * from "./RestaurantOrder"

✏️ Create src/components/FormSwitch/FormSwitch.tsx and update it to be:

import { Switch } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormSwitchProps {
  label: string
  value: boolean
  onChange: (value: boolean) => void
}

const FormSwitch: React.FC<FormSwitchProps> = ({ label, value, onChange }) => {
  const theme = useTheme()

  return (
    <Box
      style={{
        width: "100%",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        marginVertical: 8,
      }}
    >
      <Typography variant="label">{label}</Typography>
      <Switch
        thumbColor={theme.palette.primary.contrast}
        trackColor={{
          true: theme.palette.primary.strong,
          false: theme.palette.screen.soft,
        }}
      ></Switch>
    </Box>
  )
}

export default FormSwitch

✏️ Create src/components/FormSwitch/index.ts and update it to be:

export { default } from "./FormSwitch"
export * from "./FormSwitch"

Verify 1

✏️ Create src/screens/RestaurantOrder/RestaurantOrder.test.tsx and update it to be:

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

import * as restaurantHooks from "../../services/pmo/restaurant/hooks"

import RestaurantOrder from "./RestaurantOrder"

const route = {
  key: "RestaurantOrder",
  name: "RestaurantOrder",
  params: {
    slug: "bagel-restaurant",
  },
} as const

describe("Screens/RestaurantOrder", () => {
  // Mock the hooks and components used in RestaurantOrder

  const mockRestaurantResponse = {
    data: {
      name: "Bagel Restaurant",
      slug: "bagel-restaurant",
      images: {
        thumbnail: "node_modules/place-my-order-assets/images/3-thumbnail.jpg",
        owner: "node_modules/place-my-order-assets/images/1-owner.jpg",
        banner: "node_modules/place-my-order-assets/images/2-banner.jpg",
      },
      menu: {
        lunch: [
          { name: "Crab Pancakes with Sorrel Syrup", price: 35.99 },
          { name: "Steamed Mussels", price: 21.99 },
          { name: "Roasted Salmon", price: 23.99 },
        ],
        dinner: [
          { name: "Truffle Noodles", price: 14.99 },
          { name: "Spinach Fennel Watercress Ravioli", price: 35.99 },
          { name: "Herring in Lavender Dill Reduction", price: 45.99 },
        ],
      },
      address: {
        street: "285 W Adams Ave",
        city: "Detroit",
        state: "MI",
        zip: "60045",
      },
      coordinate: {
        latitude: 0,
        longitude: 0,
      },
      resources: {
        thumbnail: "api/resources/images/3-thumbnail.jpg",
        owner: "api/resources/images/4-owner.jpg",
        banner: "api/resources/images/1-banner.jpg",
      },
      _id: "5NVE3Z5MXxX3O57R",
    },
  }

  let useRestaurant: jest.SpyInstance<
    ReturnType<typeof restaurantHooks.useRestaurant>
  >
  beforeEach(() => {
    jest.resetAllMocks()
    useRestaurant = jest.spyOn(restaurantHooks, "useRestaurant")
  })

  it("renders", () => {
    useRestaurant.mockReturnValue({
      ...mockRestaurantResponse,
      error: undefined,
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Lunch Menu/i)).toBeOnTheScreen()
    expect(
      screen.getByText(mockRestaurantResponse.data.menu.lunch[0].name, {
        exact: false,
      }),
    ).toBeOnTheScreen()
    expect(screen.getByText(/Dinner Menu/i)).toBeOnTheScreen()
    expect(
      screen.getByText(mockRestaurantResponse.data.menu.dinner[0].name, {
        exact: false,
      }),
    ).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      error: undefined,
      isPending: true,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })

  it("renders error state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      error: { name: "Oops", message: "This is the error" },
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )

    expect(
      screen.getByText(/Error loading restaurant order:/),
    ).toBeOnTheScreen()
    expect(screen.getByText(/This is the error/)).toBeOnTheScreen()
  })
})

Exercise 1

  • Add RestaurantOrder to the NavigatorStack.
  • Add a link from the RestaurantOrder view to the RestaurantDetails view.
  • Update FormSwitch to pass the required props to Switch.
  • Call useState() and use the OrderItems interface to create an items state.
  • Create a function for calling setItems() with the updated items state.
  • Update subtotal to use the calculateTotal() helper function.
  • List restaurant food items for both lunch and dinner menus with switches using FormSwitchField.

Hint: The items state will look like this when populated:

const items = {
  "Menu item 1 name": 1.23, // Menu item 1 price
  "Menu item 2 name": 4.56, // Menu item 2 price
}

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/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 RestaurantOrder from "./screens/RestaurantOrder"
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
  }
  RestaurantOrder: {
    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.Screen
        name="RestaurantOrder"
        component={RestaurantOrder}
      />
    </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/components/FormSwitch/FormSwitch.tsx to be:

import { Switch } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormSwitchProps {
  label: string
  value: boolean
  onChange: (value: boolean) => void
}

const FormSwitch: React.FC<FormSwitchProps> = ({ label, value, onChange }) => {
  const theme = useTheme()

  return (
    <Box
      style={{
        width: "100%",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        marginVertical: 8,
      }}
    >
      <Typography variant="label">{label}</Typography>
      <Switch
        onValueChange={onChange}
        value={value}
        thumbColor={theme.palette.primary.contrast}
        trackColor={{
          true: theme.palette.primary.strong,
          false: theme.palette.screen.soft,
        }}
      ></Switch>
    </Box>
  )
}

export default FormSwitch

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

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

import { RestaurantsStackParamList } from "../../App"
import Loading from "../../components/Loading"
import RestaurantHeader from "../../components/RestaurantHeader"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantDetailsProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantDetails"> {}

const RestaurantDetails: React.FC<RestaurantDetailsProps> = ({ route }) => {
  const { slug } = route.params
  const navigation = useNavigation()
  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `${restaurant.name}` })
    }
  }, [restaurant, navigation])

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant details:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen>
      <RestaurantHeader restaurant={restaurant} />
      <Button
        onPress={() => {
          navigation.navigate("RestaurantOrder", { slug: slug })
        }}
      >
        Place an order
      </Button>
    </Screen>
  )
}

export default RestaurantDetails

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

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { useEffect, useState } from "react"

import { RestaurantsStackParamList } from "../../App"
import FormSwitch from "../../components/FormSwitch"
import Loading from "../../components/Loading"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantOrderProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantOrder"> {}

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC<RestaurantOrderProps> = ({ route }) => {
  const navigation = useNavigation()
  const { slug } = route.params

  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  const [items, setItems] = useState<OrderItems>({})

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `Order from ${restaurant.name}` })
    }
  }, [restaurant, navigation])

  const handleOrder = () => {
    // eslint-disable-next-line no-console
    console.info("“Place My Order” button pressed!")
  }

  const setItem = (itemId: string, isChecked: boolean, itemPrice: number) => {
    return setItems((currentItems) => {
      const updatedItems = {
        ...currentItems,
      }
      if (isChecked) {
        updatedItems[itemId] = itemPrice
      } else {
        delete updatedItems[itemId]
      }
      return updatedItems
    })
  }

  const selectedCount = Object.values(items).length
  const subtotal = calculateTotal(items)

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant order:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  if (!restaurant) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Restaurant not found</Typography>
        </Box>
      </Screen>
    )
  }

  return (
    <Screen>
      <Card title="Lunch Menu">
        {restaurant.menu.lunch.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Dinner Menu">
        {restaurant.menu.dinner.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Order Details"></Card>

      <Box padding="s">
        {subtotal === 0 ? (
          <Typography>Please choose an item.</Typography>
        ) : (
          <Typography>{selectedCount} items selected.</Typography>
        )}
      </Box>

      <Box padding="s">
        <Typography variant="heading">Total: ${subtotal.toFixed(2)}</Typography>
      </Box>

      <Box padding="s">
        <Button onPress={handleOrder}>Place My Order!</Button>
      </Box>
    </Screen>
  )
}

function calculateTotal(items: OrderItems) {
  return Object.values(items).reduce((total, itemPrice) => {
    return total + itemPrice
  }, 0)
}

export default RestaurantOrder

Objective 2: Add text fields to collect user data

Next, we want to collect the user’s name, phone number, and address as part of the order. To do this, we’ll use React Native’s TextInput component.

Screenshot of the bottom of the application restaurant order page.

Using the TextInput component

For entering text, React Native uses the TextInput component. Changes in text input are managed using the onChangeText prop, which receives the new text directly.

Here’s how you can use TextInput in your application:

import { useState } from "react"
import { Text, TextInput, View } from "react-native"

const InputExample = () => {
  const [text, setText] = useState("")

  return (
    <View>
      <Text>Enter your name:</Text>
      <TextInput onChangeText={(newText) => setText(newText)} value={text} />
    </View>
  )
}

export default InputExample

In this example, the onChangeText prop is passed a function that calls setText to store the text as state in the component.

Labelling text inputs

In the code above, we have a <Text> component in the view but it does not have a programmatic relationship with the <TextInput> itself. This means that assistive technologies (such as screen readers) won’t know to announce the Text when the TextInput has focus.

To fix this, we can use the accessibility APIs:

import { useState } from "react"
import { Text, TextInput, View } from "react-native"

const InputExample = () => {
  const [text, setText] = useState("")

  return (
    <View>
      <Text nativeID={"name"}>Enter your name:</Text>
      <TextInput
        accessibilityLabel="input"
        accessibilityLabelledBy={"name"}
        onChangeText={(newText) => setText(newText)}
        value={text}
      />
    </View>
  )
}

export default InputExample

In the code above, we add the following props:

  • nativeID on the Text to give it a unique ID.
  • accessibilityLabel to indicate the TextInput is an “edit box.”
  • accessibilityLabelledBy on the TextInput to associate it with the Text component.

These three props combined will make the input accessible to assistive technologies. For example, a screen reader would announce “Input, Edit Box for Enter your name” when the TextInput receives focus.

Creating unique IDs with the useId() Hook

There’s one minor issue with the code we have above: the nativeID prop’s value is pretty generic ("name") and might accidentally be used multiple times across multiple views, causing confusion about which component is the right one to refer to!

To solve this, we can use the useId Hook to generate a unique ID:

import { useId, useState } from "react"
import { Text, TextInput, View } from "react-native"

const InputExample = () => {
  const id = useId()
  const [text, setText] = useState("")

  return (
    <View>
      <Text nativeID={id}>Enter your name:</Text>
      <TextInput
        accessibilityLabel="input"
        accessibilityLabelledBy={id}
        onChangeText={(newText) => setText(newText)}
        value={text}
      />
    </View>
  )
}

export default InputExample

The value of useId is guaranteed to be unique within the component where it is used. This is ideal for linking related components together, as is the case with nativeID and accessibilityLabelledBy.

Setup 2

✏️ Create src/components/FormTextField/FormTextField.tsx and update it to be:

import { useId } from "react"
import { TextInput } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormTextFieldProps {
  label: string
  type?: "text"
  value: string
  onChange?: (value: string) => void
}

const FormTextField: React.FC<FormTextFieldProps> = ({
  label,
  value,
  onChange,
}) => {
  const theme = useTheme()

  return (
    <Box style={{ marginVertical: 8 }}>
      {/* Exercise: Create Text Label and update TextInput to work with label. */}
      <TextInput
        style={{
          flex: 1,
          paddingVertical: 0,
          borderBottomWidth: 1,
          borderBottomColor: theme.palette.screen.contrast,
          color: theme.palette.screen.contrast,
        }}
      />
    </Box>
  )
}

export default FormTextField

✏️ Create src/components/FormTextField/index.ts and update it to be:

export { default } from "./FormTextField"
export * from "./FormTextField"

✏️ Update src/components/FormSwitch/FormSwitch.tsx to be:

import { useId } from "react"
import { Switch } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormSwitchProps {
  label: string
  value: boolean
  onChange: (value: boolean) => void
}

const FormSwitch: React.FC<FormSwitchProps> = ({ label, value, onChange }) => {
  const theme = useTheme()

  return (
    <Box
      style={{
        width: "100%",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        marginVertical: 8,
      }}
    >
      <Typography variant="label">{label}</Typography>
      <Switch
        onValueChange={onChange}
        thumbColor={theme.palette.primary.contrast}
        trackColor={{
          true: theme.palette.primary.strong,
          false: theme.palette.screen.soft,
        }}
        value={value}
      ></Switch>
    </Box>
  )
}

export default FormSwitch

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

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { useEffect, useState } from "react"

import { RestaurantsStackParamList } from "../../App"
import FormSwitch from "../../components/FormSwitch"
import FormTextField from "../../components/FormTextField"
import Loading from "../../components/Loading"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantOrderProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantOrder"> {}

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC<RestaurantOrderProps> = ({ route }) => {
  const navigation = useNavigation()
  const { slug } = route.params

  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  const [items, setItems] = useState<OrderItems>({})
  // Exercise: Store state for new FormTextFields in RestaurantOrder.

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `Order from ${restaurant.name}` })
    }
  }, [restaurant, navigation])

  const handleOrder = () => {
    // eslint-disable-next-line no-console
    console.info("“Place My Order” button pressed!")
  }

  const setItem = (itemId: string, isChecked: boolean, itemPrice: number) => {
    return setItems((currentItems) => {
      const updatedItems = {
        ...currentItems,
      }
      if (isChecked) {
        updatedItems[itemId] = itemPrice
      } else {
        delete updatedItems[itemId]
      }
      return updatedItems
    })
  }

  const selectedCount = Object.values(items).length
  const subtotal = calculateTotal(items)

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant order:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  if (!restaurant) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Restaurant not found</Typography>
        </Box>
      </Screen>
    )
  }

  return (
    <Screen>
      <Card title="Lunch Menu">
        {restaurant.menu.lunch.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Dinner Menu">
        {restaurant.menu.dinner.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Order Details">
        {/* Exercise: Use name, phone, and address fields to create FormTextField elements. */}
      </Card>

      <Box padding="s">
        {subtotal === 0 ? (
          <Typography>Please choose an item.</Typography>
        ) : (
          <Typography>{selectedCount} items selected.</Typography>
        )}
      </Box>

      <Box padding="s">
        <Typography variant="heading">Total: ${subtotal.toFixed(2)}</Typography>
      </Box>

      <Box padding="s">
        <Button onPress={handleOrder}>Place My Order!</Button>
      </Box>
    </Screen>
  )
}

function calculateTotal(items: OrderItems) {
  return Object.values(items).reduce((total, itemPrice) => {
    return total + itemPrice
  }, 0)
}

export default RestaurantOrder

Verify 2

✏️ Create src/components/FormTextField/FormTextField.test.tsx and update it to be:

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

import FormTextField from "./FormTextField"

beforeEach(() => {
  jest.resetAllMocks()
  jest.useFakeTimers()
})

describe("Components/FormTextField", () => {
  it("renders and handles input change", async () => {
    const handleChangeMock = jest.fn()
    render(
      <FormTextField
        label="Hello!"
        onChange={handleChangeMock}
        value="response"
      />,
    )

    const user = userEvent.setup()
    expect(screen.getByText(/Hello/)).toBeTruthy()

    await user.type(screen.getByLabelText(/Hello/i), "test")

    expect(handleChangeMock).toHaveBeenCalledTimes(4)
    expect(handleChangeMock).toHaveBeenNthCalledWith(1, "responset")
    expect(handleChangeMock).toHaveBeenNthCalledWith(2, "responsee")
    expect(handleChangeMock).toHaveBeenNthCalledWith(3, "responses")
    expect(handleChangeMock).toHaveBeenNthCalledWith(4, "responset")
  })
})

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

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

import * as restaurantHooks from "../../services/pmo/restaurant/hooks"

import RestaurantOrder from "./RestaurantOrder"

const route = {
  key: "RestaurantOrder",
  name: "RestaurantOrder",
  params: {
    slug: "bagel-restaurant",
  },
} as const

describe("Screens/RestaurantOrder", () => {
  // Mock the hooks and components used in RestaurantOrder

  const mockRestaurantResponse = {
    data: {
      name: "Bagel Restaurant",
      slug: "bagel-restaurant",
      images: {
        thumbnail: "node_modules/place-my-order-assets/images/3-thumbnail.jpg",
        owner: "node_modules/place-my-order-assets/images/1-owner.jpg",
        banner: "node_modules/place-my-order-assets/images/2-banner.jpg",
      },
      menu: {
        lunch: [
          { name: "Crab Pancakes with Sorrel Syrup", price: 35.99 },
          { name: "Steamed Mussels", price: 21.99 },
          { name: "Roasted Salmon", price: 23.99 },
        ],
        dinner: [
          { name: "Truffle Noodles", price: 14.99 },
          { name: "Spinach Fennel Watercress Ravioli", price: 35.99 },
          { name: "Herring in Lavender Dill Reduction", price: 45.99 },
        ],
      },
      address: {
        street: "285 W Adams Ave",
        city: "Detroit",
        state: "MI",
        zip: "60045",
      },
      coordinate: {
        latitude: 0,
        longitude: 0,
      },
      resources: {
        thumbnail: "api/resources/images/3-thumbnail.jpg",
        owner: "api/resources/images/4-owner.jpg",
        banner: "api/resources/images/1-banner.jpg",
      },
      _id: "5NVE3Z5MXxX3O57R",
    },
  }

  let useRestaurant: jest.SpyInstance<
    ReturnType<typeof restaurantHooks.useRestaurant>
  >
  beforeEach(() => {
    jest.resetAllMocks()
    useRestaurant = jest.spyOn(restaurantHooks, "useRestaurant")
  })

  it("renders", () => {
    useRestaurant.mockReturnValue({
      ...mockRestaurantResponse,
      error: undefined,
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Lunch Menu/i)).toBeOnTheScreen()
    expect(
      screen.getByText(mockRestaurantResponse.data.menu.lunch[0].name, {
        exact: false,
      }),
    ).toBeOnTheScreen()
    expect(screen.getByText(/Dinner Menu/i)).toBeOnTheScreen()
    expect(
      screen.getByText(mockRestaurantResponse.data.menu.dinner[0].name, {
        exact: false,
      }),
    ).toBeOnTheScreen()

    expect(screen.getByLabelText(/Name/i)).toBeOnTheScreen()
    expect(screen.getByLabelText(/Phone/i)).toBeOnTheScreen()
    expect(screen.getByLabelText(/Address/i)).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      error: undefined,
      isPending: true,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })

  it("renders error state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      error: { name: "Oops", message: "This is the error" },
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantOrder route={route} />
      </NavigationContainer>,
    )

    expect(
      screen.getByText(/Error loading restaurant order:/),
    ).toBeOnTheScreen()
    expect(screen.getByText(/This is the error/)).toBeOnTheScreen()
  })
})

Exercise 2

Let’s fully implement our FormTextField component and have it:

  • Create a unique ID with useId().
  • Use a <Typography variant="label"> component with a nativeID.
  • Associate the <Typography> and <TextInput> components with the correct props for accessibility.
  • Add the onChangeText and value props to the <TextInput> component.

Additionally, let’s update the FormSwitch component to:

  • Associate the <Switch> component with the correct props for accessibility.

Finally, let’s update the RestaurantOrder component to:

  • Have state variables and setters for address, phone, and name.
  • Use <FormTextField> to create text fields for these three state variables.

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/components/FormTextField/FormTextField.tsx to be:

import { useId } from "react"
import { TextInput } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormTextFieldProps {
  label: string
  type?: "text"
  value: string
  onChange?: (value: string) => void
}

const FormTextField: React.FC<FormTextFieldProps> = ({
  label,
  value,
  onChange,
}) => {
  const theme = useTheme()
  const id = useId()

  return (
    <Box style={{ marginVertical: 8 }}>
      <Typography nativeID={id} variant="label">
        {label}
      </Typography>
      <TextInput
        accessibilityLabel="Input"
        accessibilityLabelledBy={id}
        onChangeText={onChange}
        value={value}
        style={{
          flex: 1,
          paddingVertical: 0,
          borderBottomWidth: 1,
          borderBottomColor: theme.palette.screen.contrast,
          color: theme.palette.screen.contrast,
        }}
      />
    </Box>
  )
}

export default FormTextField

✏️ Update src/components/FormSwitch/FormSwitch.tsx to be:

import { useId } from "react"
import { Switch } from "react-native"

import Box from "../../design/Box"
import { useTheme } from "../../design/theme"
import Typography from "../../design/Typography"

export interface FormSwitchProps {
  label: string
  value: boolean
  onChange: (value: boolean) => void
}

const FormSwitch: React.FC<FormSwitchProps> = ({ label, value, onChange }) => {
  const theme = useTheme()
  const id = useId()

  return (
    <Box
      style={{
        width: "100%",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        marginVertical: 8,
      }}
    >
      <Typography nativeID={id} variant="label">
        {label}
      </Typography>
      <Switch
        accessibilityLabel="Toggle"
        accessibilityLabelledBy={id}
        onValueChange={onChange}
        value={value}
        thumbColor={theme.palette.primary.contrast}
        trackColor={{
          true: theme.palette.primary.strong,
          false: theme.palette.screen.soft,
        }}
      ></Switch>
    </Box>
  )
}

export default FormSwitch

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

import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import { useEffect, useState } from "react"

import { RestaurantsStackParamList } from "../../App"
import FormSwitch from "../../components/FormSwitch"
import FormTextField from "../../components/FormTextField"
import Loading from "../../components/Loading"
import Box from "../../design/Box"
import Button from "../../design/Button"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"
import { useRestaurant } from "../../services/pmo/restaurant"

export interface RestaurantOrderProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantOrder"> {}

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC<RestaurantOrderProps> = ({ route }) => {
  const navigation = useNavigation()
  const { slug } = route.params

  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  const [items, setItems] = useState<OrderItems>({})
  const [name, setName] = useState<string>("")
  const [phone, setPhone] = useState<string>("")
  const [address, setAddress] = useState<string>("")

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `Order from ${restaurant.name}` })
    }
  }, [restaurant, navigation])

  const handleOrder = () => {
    // eslint-disable-next-line no-console
    console.info("“Place My Order” button pressed!")
  }

  const setItem = (itemId: string, isChecked: boolean, itemPrice: number) => {
    return setItems((currentItems) => {
      const updatedItems = {
        ...currentItems,
      }
      if (isChecked) {
        updatedItems[itemId] = itemPrice
      } else {
        delete updatedItems[itemId]
      }
      return updatedItems
    })
  }

  const selectedCount = Object.values(items).length
  const subtotal = calculateTotal(items)

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant order:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  if (!restaurant) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Restaurant not found</Typography>
        </Box>
      </Screen>
    )
  }

  return (
    <Screen>
      <Card title="Lunch Menu">
        {restaurant.menu.lunch.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Dinner Menu">
        {restaurant.menu.dinner.map(({ name, price }) => (
          <FormSwitch
            key={name}
            label={`${name} ($${price})`}
            onChange={(value) => setItem(name, value, price)}
            value={name in items}
          />
        ))}
      </Card>

      <Card title="Order Details">
        <FormTextField label="Name" onChange={setName} value={name} />
        <FormTextField label="Phone" onChange={setPhone} value={phone} />
        <FormTextField label="Address" onChange={setAddress} value={address} />
      </Card>

      <Box padding="s">
        {subtotal === 0 ? (
          <Typography>Please choose an item.</Typography>
        ) : (
          <Typography>{selectedCount} items selected.</Typography>
        )}
      </Box>

      <Box padding="s">
        <Typography variant="heading">Total: ${subtotal.toFixed(2)}</Typography>
      </Box>

      <Box padding="s">
        <Button onPress={handleOrder}>Place My Order!</Button>
      </Box>
    </Screen>
  )
}

function calculateTotal(items: OrderItems) {
  return Object.values(items).reduce((total, itemPrice) => {
    return total + itemPrice
  }, 0)
}

export default RestaurantOrder

Next steps

Next, in order to retain data to limit requests and migrating the same data, let’s look into Using AsyncStorage.