---
title: Bitovi Academy - Handling User Inputs and Forms
description: Use forms and inputs to collect information.
---

- ![AI implementation](https://www.bitovi.com/hubfs/AIConsultingIcon.svg)
  
  [AI implementation](https://www.bitovi.com/services/ai-consulting?hsLang=en-us)
- ![Systems engineering](https://www.bitovi.com/hubfs/icon%20-%20backend.svg)
  
  [Systems engineering](https://www.bitovi.com/services/systems-engineering-consulting?hsLang=en-us)
- ![Project management](https://www.bitovi.com/hubfs/icon%20-%20PM.svg)
  
  [Project management](https://www.bitovi.com/services/agile-project-management-consulting?hsLang=en-us)
- ![Product design](https://www.bitovi.com/hubfs/icon%20-%20design.svg)
  
  [Product design](https://www.bitovi.com/services/product-design-consulting?hsLang=en-us)
- ![Frontend development](https://www.bitovi.com/hubfs/icon%20-%20frontend.svg)
  
  [Frontend development](https://www.bitovi.com/services/frontend-development-consulting?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/digital-consulting-services?hsLang=en-us)

We're Experts in...

- [JavaScript](https://www.bitovi.com/services/frontend/javascript-consulting?hsLang=en-us)
- [AI training](https://www.bitovi.com/ai-training-for-software-engineers?hsLang=en-us)
- [Angular](https://www.bitovi.com/services/frontend/angular-consulting?hsLang=en-us)
- [Design Systems](https://www.bitovi.com/services/axure-figma-migration?hsLang=en-us)
- [React](https://www.bitovi.com/services/frontend/react-consulting?hsLang=en-us)
- [Temporal](https://www.bitovi.com/services/backend/temporal-consulting?hsLang=en-us)
- [React Native](https://www.bitovi.com/services/frontend/react-consulting/react-native?hsLang=en-us)
- [Node.js](https://www.bitovi.com/services/backend/nodejs-consulting-1?hsLang=en-us)

Showcase

![Yum! Brands](https://www.bitovi.com/hubfs/yum-showcase-link-1.png)

[View case study](https://www.bitovi.com/en/bitovi-yum-case-study?hsLang=en-us)

More Projects

- [![Levi's](https://www.bitovi.com/hubfs/levis.svg)](https://www.bitovi.com/web-application-consulting-work/levis-ecommerce-responsive-redesign?hsLang=en-us)
- [![Christie's International Real Estate](https://www.bitovi.com/hubfs/christies.svg)](https://design.bitovi.com/christies)
- [![BAFS](https://www.bitovi.com/hubfs/bafs.svg)](https://www.bitovi.com/ux-design-consulting/ux-case-studies/bafs-ppp?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/our-software-consulting-work?hsLang=en-us)

Open Source Tools

We build powerful tools and open source them to support the community.

[See what we've built →](https://www.bitovi.com/open-source?hsLang=en-us)

- [![Blog](https://www.bitovi.com/hubfs/icon%20-%20blog.svg)
  
  BlogWe post about perfecting software delivery
  
  ](https://bitovi.com/blog)
- [![Partnerships](https://www.bitovi.com/hubfs/Handshake-1.svg)
  
  PartnershipsLearn about Bitovi's technology partners
  
  ](https://www.bitovi.com/partnerships?hsLang=en-us)
- [![Academy](https://www.bitovi.com/hubfs/limbo/icons/icon-academy.svg)
  
  AcademyFree courses to build delivery skills
  
  ](https://www.bitovi.com/academy/?hsLang=en-us)
- [![Open source tools](https://www.bitovi.com/hubfs/icon%20-%20open%20source.svg)
  
  Open source toolsUse our tools or contribute to our open source mission.
  
  ](https://www.bitovi.com/open-source?hsLang=en-us)

Let's Connect

- [![Discord](https://www.bitovi.com/hubfs/DiscordLogo.svg)
  
  Discord
  
  ](https://discord.gg/J7ejFsZnJ4)
- [![LinkedIn](https://www.bitovi.com/hubfs/LinkedinLogo.svg)
  
  LinkedIn
  
  ](https://www.linkedin.com/company/bitovi/)
- [![GitHub](https://www.bitovi.com/hubfs/GithubLogo.svg)
  
  GitHub
  
  ](https://github.com/bitovi/)

![Eggbot](https://www.bitovi.com/hubfs/build_assets/bitovi-limbo-cms-react/338/js_client_assets/assets/eggbot-LTGhdSGL.png)

Name *

Work Email *

Phone

What's your project?

Send

### Contact Us

(312) 620-0386contact@bitovi.com

- [ Advanced JavaScript ](https://www.bitovi.com/academy/learn-advanced-javascript.html?hsLang=en-us)
- [ DOM and jQuery ](https://www.bitovi.com/academy/learn-dom-jquery.html?hsLang=en-us)
- [ Debug JavaScript ](https://www.bitovi.com/academy/learn-to-debug-javascript.html?hsLang=en-us)
- [ Learn TypeScript ](https://www.bitovi.com/academy/learn-typescript.html?hsLang=en-us)
- [ Learn React Native ](https://www.bitovi.com/academy/learn-react-native.html?hsLang=en-us)
- [ Learn Web Components ](https://www.bitovi.com/academy/learn-web-components.html?hsLang=en-us)
- [ Agile Program Management With Jira ](https://www.bitovi.com/academy/learn-agile-program-management-with-jira.html?hsLang=en-us)
- [ Learn RxJS ](https://www.bitovi.com/academy/learn-rxjs.html?hsLang=en-us)
- [ Giving a Training ](https://www.bitovi.com/academy/training.html?hsLang=en-us)
- [ Learn Technology Consulting ](https://www.bitovi.com/academy/learn-technology-consulting.html?hsLang=en-us)
- [ Learn React ](https://www.bitovi.com/academy/learn-react.html?hsLang=en-us) 
    - [ Introduction to React ](https://www.bitovi.com/academy/learn-react/intro-to-react.html?hsLang=en-us)
    - [ Setting Up Your Environment ](https://www.bitovi.com/academy/learn-react/setting-up-your-environment.html?hsLang=en-us)
    - [ Introduction to JSX ](https://www.bitovi.com/academy/learn-react/intro-to-jsx.html?hsLang=en-us)
    - [ Building Components ](https://www.bitovi.com/academy/learn-react/components.html?hsLang=en-us)
    - [ Passing Props ](https://www.bitovi.com/academy/learn-react/props.html?hsLang=en-us)
    - [ Routing in React ](https://www.bitovi.com/academy/learn-react/routing.html?hsLang=en-us)
    - [ Styling in React ](https://www.bitovi.com/academy/learn-react/styling-in-react.html?hsLang=en-us)
    - [ Managing State in React ](https://www.bitovi.com/academy/learn-react/stateful-hooks.html?hsLang=en-us)
    - [ Making HTTP Requests ](https://www.bitovi.com/academy/learn-react/making-http-requests.html?hsLang=en-us)
    - [ Creating Nested Routes ](https://www.bitovi.com/academy/learn-react/nested-routes.html?hsLang=en-us)
    - [ Handling User Inputs and Forms ](https://www.bitovi.com/academy/learn-react/controlled-vs-uncontrolled.html?hsLang=en-us)
    - [ Testing in React ](https://www.bitovi.com/academy/learn-react/testing.html?hsLang=en-us)
    - [ Additional Resources ](https://www.bitovi.com/academy/learn-react/additional-resources.html?hsLang=en-us)
- [ Learn NgRx ](https://www.bitovi.com/academy/learn-ngrx.html?hsLang=en-us)
- [ Learn Graphql ](https://www.bitovi.com/academy/learn-graphql.html?hsLang=en-us)
- [ Learn Docker ](https://www.bitovi.com/academy/learn-docker.html?hsLang=en-us)
- [ Learn CanJS ](https://www.bitovi.com/academy/learn-canjs.html?hsLang=en-us)
- [ Learn Angular ](https://www.bitovi.com/academy/learn-angular.html?hsLang=en-us)
- [ Advanced TypeScript ](https://www.bitovi.com/academy/advanced-typescript.html?hsLang=en-us)

[View More Courses](https://www.bitovi.com/academy/#courses) [ ]

# Handling User Inputs and Forms `page`

Use forms and inputs to collect information.

## Overview

In this section, we will:

- Learn about controlled vs. uncontrolled inputs
- Work with change events
- Use TypeScript’s `Record` interface
- Set state using an updater function
- Updating reference types and rendering
- Learn about the `useId()` Hook

## Objective 1: Add checkboxes to order menu items

Now that we have our `RestaurantOrder` page, let’s start building the form for placing an order!

We’ll start with checkboxes 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.

### Controlled vs. uncontrolled inputs

React has special handling for `<input>` components that allow developers to create “controlled” or “uncontrolled” inputs. An input is **uncontrolled** when its `value` (or `checked`) prop is not set, and it does not have a handler set for the `onChange` prop; an initial value may be set using the `defaultValue` prop.

An input is **controlled** when both its `value` (or `checked`) and `onChange` props have values set. The term “controlled” refers to the fact that the value of the input is controlled by React. Most of the time, we want our `<input>` components to be controlled with their value stored in a state variable.

**Note:** If an `<input>` only has the `value` or `onChange` prop set, React will log a warning to the console in development mode.

Let’s take a look at an example of a controlled input:

```
import { useState } from "react"

const ControlledInput: React.FC = () => {
  const [name, setName] = useState("")
  return (
    <label>
      Name:
      <input
        type="text"
        onChange={(event) => setName(event.target.value)}
        value={name}
      />
    </label>
  )
}

```

Controlled components aren’t allowed to have a value of `null` or `undefined`. To set an input with “no value,” use an empty string: `""`.

### Working with change events

In the previous example, the `<input>` prop `onChange` had its value set to a function known as an “event handler.”

```
import { useState } from "react"

const ControlledInput: React.FC = () => {
  const [name, setName] = useState("")
  return (
    <label>
      Name:
      <input
        type="text"
        onChange={(event) => setName(event.target.value)}
        value={name}
      />
    </label>
  )
}

```

When an event occurs, such as a user typing in an input field, an `event` object is passed to the event handler function. This event object contains various properties and methods that provide information about the event.

One such property is `target`, which is a reference to the DOM element that triggered the event. In the case of an input field, target would refer to the specific `<input>` element where the user is typing.

The property `event.target.value` is particularly important when dealing with input fields. The value property here holds the current content of the input field. It represents what the user has entered or selected. When you access `event.target.value` in your event handler function, you’re essentially retrieving the latest input provided by the user. This is commonly used to update the state of the component with the new input value, ensuring that the component’s state is in sync with what the user is entering.

For most input types, you’ll want to use `event.target.value` to get the value entered. But there are exceptions! For `<input type="checkbox">`, you’ll want to use `event.target.checked` instead:

```
import { useState } from "react"

const TodoItem: React.FC = () => {
  const [isCompleted, setIsCompleted] = useState(false)
  return (
    <label>
      <input
        type="checkbox"
        checked={isCompleted}
        onChange={(event) => setIsCompleted(event.target.checked)}
      />
      Completed
    </label>
  )
}
```

### 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"

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

type SelectedItems = Record<string, number>

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

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

  return (
    <form>
      {landmarks.map((landmark) => {
        return (
          <label key={landmark.id}>
            {landmark.name}:
            <input
              type="checkbox"
              checked={selected[landmark.name]}
              onChange={(event) =>
                handleChange(landmark.name, event.target.checked)
              }
            />
          </label>
        )
      })}
    </form>
  )
}

```

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"

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

type SelectedItems = Record<string, number>

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 (
    <form>
      {landmarks.map((landmark) => {
        return (
          <label key={landmark.id}>
            {landmark.name}:
            <input
              type="checkbox"
              checked={selected[landmark.name]}
              onChange={(event) =>
                handleSelectedChange(landmark.name, event.target.checked)
              }
            />
          </label>
        )
      })}
    </form>
  )
}

```

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 checkbox was checked.

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.

```
// Adding an item when state (`current`) is an array.
setSelectedOrders((current) => {
  const next = [...current, newOrder]
  return next
})

// Replacing 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/pages/RestaurantOrder/RestaurantOrder.tsx** to be:

```
import { useState } from "react"
import { useParams } from "react-router-dom"
import RestaurantHeader from "../../components/RestaurantHeader"
import { useRestaurant } from "../../services/pmo/restaurant"

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC = () => {
  const params = useParams() as { slug: string }

  const restaurant = useRestaurant(params.slug)

  if (restaurant.isPending) {
    return (
      <p aria-live="polite" className="loading">
        Loading restaurant…
      </p>
    )
  }

  if (restaurant.error) {
    return (
      <p aria-live="polite" className="error">
        Error loading restaurant: {restaurant.error.message}
      </p>
    )
  }

  if (!restaurant.data) {
    return <p aria-live="polite">No restaurant found.</p>
  }

  const subtotal = 0 // Use calculateTotal here.

  return (
    <>
      <RestaurantHeader restaurant={restaurant.data} />

      <div className="order-form">
        <h3>Order from {restaurant.data.name}!</h3>

        <form>
          {subtotal === 0 && (
            <p className="info text-error">Please choose an item.</p>
          )}

          <h4>Lunch Menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.lunch.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input type="checkbox" />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <h4>Dinner menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.dinner.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input type="checkbox" />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <div className="submit">
            <h4>Total: ${subtotal ? subtotal.toFixed(2) : "0.00"}</h4>
          </div>
        </form>
      </div>
    </>
  )
}

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

export default RestaurantOrder

```

### Verify 1

These tests will pass when the solution has been implemented properly.

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

```
import "@testing-library/jest-dom"
import type { ReactNode } from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { MemoryRouter } from "react-router-dom"
import { describe, expect, it, vi } from "vitest"
import type { ArgumentsType, Mock } from "vitest"
import RestaurantOrder from "./RestaurantOrder"

import { useRestaurant } from "../../services/pmo/restaurant"

const mockUseRestaurant = useRestaurant as Mock<ArgumentsType<typeof useRestaurant>, ReturnType<typeof useRestaurant>>

// Mock the hooks and components used in RestaurantOrder
vi.mock("../../services/pmo/restaurant", () => ({
  useRestaurant: vi.fn(),
}))

vi.mock("../../components/RestaurantHeader", () => ({
  default: vi.fn(() => (
    <div data-testid="mock-restaurant-header">Mock RestaurantHeader</div>
  )),
}))

const mockRestaurantData = {
  data: {
    _id: "1",
    name: "Test Restaurant",
    slug: "test-restaurant",
    images: { owner: "owner.jpg", banner: "", thumbnail: "" },
    menu: {
      lunch: [
        { name: "Lunch Item 1", price: 10 },
        { name: "Lunch Item 2", price: 15 },
      ],
      dinner: [
        { name: "Dinner Item 1", price: 20 },
        { name: "Dinner Item 2", price: 25 },
      ],
    },
  },
  isPending: false,
  error: undefined,
}

const renderWithRouter = (
  ui: ReactNode,
  { route = "/restaurants/test-restaurant" } = {},
) => {
  window.history.pushState({}, "Test page", route)
  return render(ui, {
    wrapper: ({ children }) => (
      <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
    ),
  })
}

describe("RestaurantOrder component", () => {
  it("renders loading state", () => {
    mockUseRestaurant.mockReturnValue({ data: undefined, isPending: true, error: undefined })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/Loading restaurant…/i)).toBeInTheDocument()
  })

  it("renders error state", () => {
    mockUseRestaurant.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: "loading-error", message: "Error loading" },
    })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/Error loading restaurant/i)).toBeInTheDocument()
  })

  it("renders no restaurant found state", () => {
    mockUseRestaurant.mockReturnValue({ data: undefined, isPending: false, error: undefined })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/No restaurant found/i)).toBeInTheDocument()
  })

  it("renders the RestaurantHeader when data is available", () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    renderWithRouter(<RestaurantOrder />)

    expect(screen.getByTestId("mock-restaurant-header")).toBeInTheDocument()
  })

  it("renders the order form when restaurant data is available", () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    render(<RestaurantOrder />)

    expect(screen.getByTestId("mock-restaurant-header")).toBeInTheDocument()
    expect(screen.getByText("Order from Test Restaurant!")).toBeInTheDocument()
    expect(screen.getAllByRole("checkbox").length).toBe(4) // 2 lunch + 2 dinner items
  })

  it("updates subtotal when menu items are selected", async () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    render(<RestaurantOrder />)

    const checkboxes = screen.getAllByRole("checkbox")
    await userEvent.click(checkboxes[0]) // Select 'Lunch Item 1' (price: 10)

    expect(screen.getByText("Total: $10.00")).toBeInTheDocument()

    await userEvent.click(checkboxes[2]) // Select 'Dinner Item 1' (price: 20)

    expect(screen.getByText("Total: $30.00")).toBeInTheDocument()
  })
})

```

### Exercise 1

- Call `useState()` and use the `OrderItems` interface to create an `items` state.
- Create a function for calling `setItems()` with the updated `items` state.
- Add the `checked` and `onChange` props to all the checkboxes.
- Update `subtotal` to use the `calculateTotal()` helper function.

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
}
```

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/01-problem?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/01-problem?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) to do this exercise in an online code editor.

### Solution 1

Click to see the solution

✏️ Update **src/pages/RestaurantOrder/RestaurantOrder.tsx** to be:

```
import { useState } from "react"
import { useParams } from "react-router-dom"
import RestaurantHeader from "../../components/RestaurantHeader"
import { useRestaurant } from "../../services/pmo/restaurant"

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC = () => {
  const params = useParams() as { slug: string }

  const restaurant = useRestaurant(params.slug)

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

  if (restaurant.isPending) {
    return (
      <p aria-live="polite" className="loading">
        Loading restaurant…
      </p>
    )
  }

  if (restaurant.error) {
    return (
      <p aria-live="polite" className="error">
        Error loading restaurant: {restaurant.error.message}
      </p>
    )
  }

  if (!restaurant.data) {
    return <p aria-live="polite">No restaurant found.</p>
  }

  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 subtotal = calculateTotal(items)

  return (
    <>
      <RestaurantHeader restaurant={restaurant.data} />

      <div className="order-form">
        <h3>Order from {restaurant.data.name}!</h3>

        <form>
          {subtotal === 0 && (
            <p className="info text-error">Please choose an item.</p>
          )}

          <h4>Lunch Menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.lunch.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <h4>Dinner menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.dinner.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <div className="submit">
            <h4>Total: ${subtotal ? subtotal.toFixed(2) : "0.00"}</h4>
          </div>
        </form>
      </div>
    </>
  )
}

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

export default RestaurantOrder

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/01-solution?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/01-solution?file=src/pages/RestaurantOrder/RestaurantOrder.tsx).

## Objective 2: Create a reusable text field component

The order form is going to be made up of many input fields with labels. Rather than repeat multiple input components over and over, let’s compose that structure in a single component named `FormTextField`. Creating this component will involve using some of what we’ve learned from prior lessons.

### The `useId()` Hook

Since the value of every `id` attribute in an HTML document must be unique, this Hook is useful in creating a unique identifier string that can be used as the value for an `id` prop.

Let’s say you’re rendering a component that has a `label` that needs to be associated with an `input`:

```
<label for="name"> Name </label> <input id="name" type="text" />
```

Every ID has to be unique in an HTML page, but `name` might clash with another element in a page. To avoid this issue in React, we can get a unique ID with `useId()`:

```
import { useId } from "react"

const Form: React.FC = () => {
  const id = useId()

  return (
    <>
      <label htmlFor={id}>Name</label>
      <input id={id} type="text" />
    </>
  )
}
```

The value of `useId` is guaranteed to be unique within the component where it is used; this ideal for linking related components together.

### Setup 2

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

```
import { useId } from "react"

const FormTextField: React.FC<{}> = ({}) => {
  return (
    <div className="form-group">
      <label className="control-label">Label:</label>

      <input className="form-control" />
    </div>
  )
}

export default FormTextField

```

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

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

```

### Verify 2

These tests will pass when the solution has been implemented properly.

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

```
import "@testing-library/jest-dom"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { describe, expect, it, vi } from "vitest"
import FormTextField from "./FormTextField"

describe("FormTextField component", () => {
  const mockOnChange = vi.fn()

  it("renders with correct label and type", () => {
    render(
      <FormTextField
        label="Test Label"
        type="text"
        value=""
        onChange={mockOnChange}
      />,
    )

    expect(screen.getByLabelText(/Test Label:/i)).toBeInTheDocument()
    expect(screen.getByLabelText(/Test Label:/i)).toHaveAttribute(
      "type",
      "text",
    )
  })

  it("renders with the correct value", () => {
    render(
      <FormTextField
        label="Test Label"
        type="text"
        value="Test Value"
        onChange={mockOnChange}
      />,
    )

    expect(screen.getByLabelText(/Test Label:/i)).toHaveValue("Test Value")
  })

  it("calls onChange prop when input changes", async () => {
    render(
      <FormTextField
        label="Test Label"
        type="text"
        value=""
        onChange={mockOnChange}
      />,
    )

    await userEvent.type(screen.getByLabelText(/Test Label:/i), "New")

    expect(mockOnChange).toHaveBeenCalledTimes(3)
    expect(mockOnChange).toHaveBeenCalledWith("N")
    expect(mockOnChange).toHaveBeenCalledWith("e")
    expect(mockOnChange).toHaveBeenCalledWith("w")
  })

  it("respects different input types", () => {
    render(
      <FormTextField
        label="Email"
        type="email"
        value=""
        onChange={mockOnChange}
      />,
    )

    expect(screen.getByLabelText(/Email:/i)).toHaveAttribute("type", "email")
  })
})

```

### Exercise 2

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

- Accept `label`, `onChange`, `type`, and `value` props
- Create a unique ID with `useId()`
- Associate the `<label>` and `<input>` elements with `htmlFor` and `id` props
- Add the `onChange`, `type`, and `value` props to the `<input>` element

Hint: The `onChange` prop type can be defined as `(data: string) => void`

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/02-problem?file=src/components/FormTextField/FormTextField.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/02-problem?file=src/components/FormTextField/FormTextField.tsx) to do this exercise in an online code editor.

### Solution 2

Click to see the solution

✏️ Update **src/components/FormTextField/FormTextField.tsx** to be:

```
import { useId } from "react"

const FormTextField: React.FC<{
  label: string
  onChange: (data: string) => void
  type: string
  value: string
}> = ({ label, type, value, onChange }) => {
  const id = useId()

  return (
    <div className="form-group">
      <label className="control-label" htmlFor={id}>
        {label}:
      </label>

      <input
        className="form-control"
        id={id}
        onChange={(event) => onChange(event.target.value)}
        type={type}
        value={value}
      />
    </div>
  )
}

export default FormTextField

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/02-solution?file=src/components/FormTextField/FormTextField.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/02-solution?file=src/components/FormTextField/FormTextField.tsx).

## Objective 3: Integrate `FormTextField` into `RestaurantOrder`

Finally we’ll update the form to incorporate the `FormTextField` component so users can create and submit an order to the restaurant. We need to fill the form with input fields and handle the submit button.

### Setup 3

✏️ Update **src/pages/RestaurantOrder/RestaurantOrder.tsx** to be:

```
import type { FormEvent} from "react";
import { useState } from "react"
import { useParams } from "react-router-dom"
import FormTextField from "../../components/FormTextField"
import RestaurantHeader from "../../components/RestaurantHeader"
import { useRestaurant } from "../../services/pmo/restaurant"

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC = () => {
  const params = useParams() as { slug: string }

  const restaurant = useRestaurant(params.slug)

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

  if (restaurant.isPending) {
    return (
      <p aria-live="polite" className="loading">
        Loading restaurant…
      </p>
    )
  }

  if (restaurant.error) {
    return (
      <p aria-live="polite" className="error">
        Error loading restaurant: {restaurant.error.message}
      </p>
    )
  }

  if (!restaurant.data) {
    return <p aria-live="polite">No restaurant found.</p>
  }

  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 handleSubmit = (event: FormEvent) => {
    event.preventDefault()
    alert("Order submitted!")
  }

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

  return (
    <>
      <RestaurantHeader restaurant={restaurant.data} />

      <div className="order-form">
        <h3>Order from {restaurant.data.name}!</h3>

        <form onSubmit={(event) => handleSubmit(event)}>
          {subtotal === 0 ? (
            <p className="info text-error">Please choose an item.</p>
          ) : (
            <p className="info text-success">{selectedCount} selected.</p>
          )}

          <h4>Lunch Menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.lunch.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <h4>Dinner menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.dinner.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <div className="submit">
            <h4>Total: ${subtotal ? subtotal.toFixed(2) : "0.00"}</h4>
            <button className="btn" type="submit">
              Place My Order!
            </button>
          </div>
        </form>
      </div>
    </>
  )
}

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

export default RestaurantOrder

```

### Verify 3

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

```
import "@testing-library/jest-dom"
import type { ReactNode } from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { MemoryRouter } from "react-router-dom"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import type { ArgumentsType, Mock } from "vitest"
import RestaurantOrder from "./RestaurantOrder"

import { useRestaurant } from "../../services/pmo/restaurant"

const mockUseRestaurant = useRestaurant as Mock<ArgumentsType<typeof useRestaurant>, ReturnType<typeof useRestaurant>>

// Mock the hooks and components used in RestaurantOrder
vi.mock("../../services/pmo/restaurant", () => ({
  useRestaurant: vi.fn(),
}))

vi.mock("../../components/FormTextField", () => ({
  default: vi.fn(({ label, onChange, type, value }) => (
    <div className="form-group">
      <label htmlFor={`form-field-${label.toLowerCase()}`}>{label}</label>
      <input
        id={`form-field-${label.toLowerCase()}`}
        data-testid={`form-field-${label.toLowerCase()}`}
        className="form-control"
        onChange={(event) => onChange(event.target.value)}
        type={type}
        value={value}
      />
    </div>
  )),
}))

vi.mock("../../components/RestaurantHeader", () => ({
  default: vi.fn(() => (
    <div data-testid="mock-restaurant-header">Mock RestaurantHeader</div>
  )),
}))

// Mocking the global fetch function
const mockAlert = vi.fn()

// @ts-ignore `global` exists in node environments like our test runner.
global.alert = mockAlert

beforeEach(() => {
  mockAlert.mockClear()
})

afterEach(() => {
  mockAlert.mockClear()
})

const mockRestaurantData = {
  data: {
    _id: "1",
    name: "Test Restaurant",
    slug: "test-restaurant",
    images: { owner: "owner.jpg", banner: "", thumbnail: "" },
    menu: {
      lunch: [
        { name: "Lunch Item 1", price: 10 },
        { name: "Lunch Item 2", price: 15 },
      ],
      dinner: [
        { name: "Dinner Item 1", price: 20 },
        { name: "Dinner Item 2", price: 25 },
      ],
    },
  },
  isPending: false,
  error: undefined,
}

const renderWithRouter = (
  ui: ReactNode,
  { route = "/restaurants/test-restaurant" } = {},
) => {
  window.history.pushState({}, "Test page", route)
  return render(ui, {
    wrapper: ({ children }) => (
      <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
    ),
  })
}

describe("RestaurantOrder component", () => {
  it("renders loading state", () => {
    mockUseRestaurant.mockReturnValue({ data: undefined, isPending: true, error: undefined })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/Loading restaurant…/i)).toBeInTheDocument()
  })

  it("renders error state", () => {
    mockUseRestaurant.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: "loading-error", message: "Error loading" },
    })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/Error loading restaurant/i)).toBeInTheDocument()
  })

  it("renders no restaurant found state", () => {
    mockUseRestaurant.mockReturnValue({ data: undefined, isPending: false, error: undefined })
    renderWithRouter(<RestaurantOrder />)
    expect(screen.getByText(/No restaurant found/i)).toBeInTheDocument()
  })

  it("renders the RestaurantHeader when data is available", () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    renderWithRouter(<RestaurantOrder />)

    expect(screen.getByTestId("mock-restaurant-header")).toBeInTheDocument()
  })

  it("renders the order form when restaurant data is available", () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    render(<RestaurantOrder />)

    expect(screen.getByTestId("mock-restaurant-header")).toBeInTheDocument()
    expect(screen.getByText("Order from Test Restaurant!")).toBeInTheDocument()
    expect(screen.getAllByRole("checkbox").length).toBe(4) // 2 lunch + 2 dinner items
  })

  it("updates subtotal when menu items are selected", async () => {
    mockUseRestaurant.mockReturnValue(mockRestaurantData)
    render(<RestaurantOrder />)

    const checkboxes = screen.getAllByRole("checkbox")
    await userEvent.click(checkboxes[0]) // Select 'Lunch Item 1' (price: 10)

    expect(screen.getByText("Total: $10.00")).toBeInTheDocument()

    await userEvent.click(checkboxes[2]) // Select 'Dinner Item 1' (price: 20)

    expect(screen.getByText("Total: $30.00")).toBeInTheDocument()
  })

  it("updates form fields", async () => {
    renderWithRouter(<RestaurantOrder />)

    await userEvent.type(screen.getByTestId("form-field-name"), "John Doe")
    expect(screen.getByTestId("form-field-name")).toHaveValue("John Doe")

    await userEvent.type(
      screen.getByTestId("form-field-address"),
      "123 Main St",
    )
    expect(screen.getByTestId("form-field-address")).toHaveValue("123 Main St")

    await userEvent.type(screen.getByTestId("form-field-phone"), "555-1234")
    expect(screen.getByTestId("form-field-phone")).toHaveValue("555-1234")
  })

  it("handles form submission", async () => {
    const submitSpy = vi.fn()
    renderWithRouter(<RestaurantOrder />)

    const submitButton = screen.getByRole("button", {
      name: /Place My Order!/i,
    })
    const form = submitButton.closest("form")
    expect(form).toBeInTheDocument() // Ensure the form is found

    if (form) {
      form.onsubmit = submitSpy
    }

    await userEvent.click(submitButton)
    expect(submitSpy).toHaveBeenCalledTimes(1)
  })
})

```

### Exercise 3

- Create state variables and setters for `address`, `name`, and `phone`.
- Use `<FormTextField>` to create input fields for these three state variables.

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/03-problem?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/03-problem?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) to do this exercise in an online code editor.

### Solution 3

Click to see the solution

✏️ Update **src/pages/RestaurantOrder/RestaurantOrder.tsx** to be:

```
import type { FormEvent} from "react";
import { useState } from "react"
import { useParams } from "react-router-dom"
import FormTextField from "../../components/FormTextField"
import RestaurantHeader from "../../components/RestaurantHeader"
import { useRestaurant } from "../../services/pmo/restaurant"

type OrderItems = Record<string, number>

const RestaurantOrder: React.FC = () => {
  const params = useParams() as { slug: string }

  const restaurant = useRestaurant(params.slug)

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

  if (restaurant.isPending) {
    return (
      <p aria-live="polite" className="loading">
        Loading restaurant…
      </p>
    )
  }

  if (restaurant.error) {
    return (
      <p aria-live="polite" className="error">
        Error loading restaurant: {restaurant.error.message}
      </p>
    )
  }

  if (!restaurant.data) {
    return <p aria-live="polite">No restaurant found.</p>
  }

  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 handleSubmit = (event: FormEvent) => {
    event.preventDefault()
    alert("Order submitted!")
  }

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

  return (
    <>
      <RestaurantHeader restaurant={restaurant.data} />

      <div className="order-form">
        <h3>Order from {restaurant.data.name}!</h3>

        <form onSubmit={(event) => handleSubmit(event)}>
          {subtotal === 0 ? (
            <p className="info text-error">Please choose an item.</p>
          ) : (
            <p className="info text-success">{selectedCount} selected.</p>
          )}

          <h4>Lunch Menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.lunch.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <h4>Dinner menu</h4>
          <ul className="list-group">
            {restaurant.data.menu.dinner.map(({ name, price }) => (
              <li key={name} className="list-group-item">
                <label>
                  <input
                    checked={name in items}
                    onChange={(event) =>
                      setItem(name, event.target.checked, price)
                    }
                    type="checkbox"
                  />
                  {name}
                  <span className="badge">{price}</span>
                </label>
              </li>
            ))}
          </ul>

          <FormTextField
            label="Name"
            onChange={setName}
            type="text"
            value={name}
          />
          <FormTextField
            label="Address"
            onChange={setAddress}
            type="text"
            value={address}
          />
          <FormTextField
            label="Phone"
            onChange={setPhone}
            type="tel"
            value={phone}
          />

          <div className="submit">
            <h4>Total: ${subtotal ? subtotal.toFixed(2) : "0.00"}</h4>
            <button className="btn" type="submit">
              Place My Order!
            </button>
          </div>
        </form>
      </div>
    </>
  )
}

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

export default RestaurantOrder

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/03-solution?file=src/pages/RestaurantOrder/RestaurantOrder.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/11-controlled-vs-uncontrolled/03-solution?file=src/pages/RestaurantOrder/RestaurantOrder.tsx).

## Next steps

Next, let’s learn how to [write tests with React Testing Library](https://www.bitovi.com/academy/learn-react/testing.html?hsLang=en-us).

###### On this page...

###### Get help

If you need [help](https://discord.gg/J7ejFsZnJ4) with an exercise, please reach out to us on [Discord](https://discord.gg/J7ejFsZnJ4)

If you find a bug, please [create an issue](https://github.com/bitovi/academy/issues/new) or email [contact@bitovi.com](mailto:contact@bitovi.com?Subject=Training%20Suggestion)

## Join the Bitovi Newsletter

 Do you want to receive updates about new Bitovi Academy courses, Bitovi open source projects, and dev community news? If so, please provide your email. We won’t spam you or give it away for any reason!  
*By providing your email you agree to receive communications from Bitovi*

![](https://bitovi.github.io/academy/static/img/close.svg)

```json
{
     "@context": "http://schema.org",
     "@type": "Organization",
     "mainEntityOfPage":{
          "@type":"WebPage",
          "@id":"https://www.bitovi.com/academy/learn-react/controlled-vs-uncontrolled.html",
          "description": "Use forms and inputs to collect information. 
"
     },
     "url": "http://bitovi.com",
     "logo": "https://www.bitovi.com/hubfs/bitovi-logo-23-1.svg",
     "image": "https://www.bitovi.com/hubfs/bitovi-logo-x2.png",
     "name": "Bitovi Web App Consulting",
     "alternateName": "Bitovi",
      "naics": "541511",
      "description": "Bitovi is a UX, UI design and front-end JavaScript development consulting company",
     "address": {
          "@type": "PostalAddress",
          "streetAddress": "1134 Pine Tree Lane ",
          "addressLocality": "Libertyville",
          "addressRegion": "IL",
          "addressCountry": "United States",
          "postalCode": "60048"
     },
     "areaServed": {
          "@type": "GeoCircle",
          "geoMidpoint": {
               "@type": "GeoCoordinates",
               "latitude": "41.8781",
               "longitude": "87.6298"
          },
          "geoRadius": "5000 km"
     },
     "telephone": "312-620-0386",
     "email": "contact@bitovi.com",
     "sameAs": [
          "https://www.facebook.com/BitoviLLC/",
          "https://twitter.com/bitovi",
          "https://www.linkedin.com/company/bitovi"
     ]
}
```