Passing Props page

Learn how to provide data to a component through props.

Overview

In this section, we will:

  • Understand what props are and how they work in React
  • Define a component’s props using TypeScript interfaces
  • Use props within a child component
  • Pass props from a parent component to a child component

Objective: Use props to make more maintainable components

We’ve taken a great step to make our code more readable and our app more maintainable by creating the RestaurantList component.

Let’s keep the good refactoring rolling by creating a ListItem component to house the JSX used to render each restaurant in the list.

Using component props

In React, props (short for “properties”) are how we pass data from a parent component to a child component. Since function React components are fundamentally JavaScript functions, you can think of props like the arguments you pass to a function.

Also note that React component props are not the same as the "properties" that exist on a DOM element.

To receive props, function components must implement a React API that allows an optional argument of type object that’s named props.

The properties on the props object—individually called a “prop”—can include whatever data the child component needs to make the component work. The property values can be any type, including functions and other React components.

We’re using TypeScript in our project, so we can create an interface for props and use it in the definition of a function component.

Let’s create a SubmitButton component to see props in action:

interface SubmitButtonProps {
  label: string
  onClick: () => void
}

const SubmitButton: React.FC<SubmitButtonProps> = (props) => {
  const { label, onClick } = props
  return (
    <button onClick={onClick} type="submit">
      {label}
    </button>
  )
}

In this example, SubmitButtonProps is an interface that defines the types for label (a string) and onClick (a function). Our SubmitButton component then uses these props to display a button with a label and a click action.

The example above illustrates how props are passed to component as an argument.

However, more commonly (and for the rest of this course) you will see props destructured in the function parameters:

interface SubmitButtonProps {
  label: string
  onClick: () => void
}

const SubmitButton: React.FC<SubmitButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>
}

Passing component props

Now, how do we use this SubmitButton? In JSX syntax a component’s props look like an HTML tag’s "attributes" and accept a value.

  • If a prop’s value type is a string then the prop value is set using quotes.
  • Any other type of prop value is set using braces with the value inside.

In the example below, the label prop accepts a string. so the value is surrounded by double quotes. The onClick prop accepts a function, so the function value is surrounded by braces.

Here’s how to use our SubmitButton:

const content = (
  <SubmitButton label="Activate" onClick={() => alert("Activated!")} />
)

In the example above, we’re setting the label prop to the string “Activate” and the onClick prop to a function that displays an alert.

Reserved prop names

There are two prop names that you cannot use and are reserved by React:

  • children: this prop is automatically provided by React to every component. We will see this prop in later examples.

  • key: this prop is one you’ve seen before in the Introduction to JSX module! It’s not actually part of the component’s props in a traditional sense. Instead, it’s used by React itself to manage lists of elements and identify which items have changed, been added, or been removed.

Setup

✏️ Create src/pages/RestaurantList/ListItem.tsx and update it to contain:

interface ListItemProps {
  address?: {
    city: string
    state: string
    street: string
    zip: string
  }
  name: string
  slug: string
  thumbnail: string
}

const ListItem: React.FC<ListItemProps> = () => {
  return <></>
}

export default ListItem

✏️ Update src/pages/RestaurantList/RestaurantList.tsx to import ListItem:

import CheeseThumbnail from "place-my-order-assets/images/2-thumbnail.jpg"
import PoutineThumbnail from "place-my-order-assets/images/4-thumbnail.jpg"
import ListItem from "./ListItem"

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

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>
        {restaurants.data ? (
          restaurants.data.map(({ _id, address, images, name, slug }) => (
            <div key={_id} className="restaurant">
              <img src={images.thumbnail} alt="" width="100" height="100" />
              <h3>{name}</h3>

              {address && (
                <div className="address">
                  {address.street}
                  <br />
                  {address.city}, {address.state} {address.zip}
                </div>
              )}

              <div className="hours-price">
                $$$
                <br />
                Hours: M-F 10am-11pm
                <span className="open-now">Open Now</span>
              </div>

              <a className="btn" href={`/restaurants/${slug}`}>
                Details
              </a>
              <br />
            </div>
          ))
        ) : (
          <p>No restaurants.</p>
        )}
      </div>
    </>
  )
}

export default RestaurantList

Verify

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

✏️ Create src/pages/RestaurantList/ListItem.test.tsx and update it to be:

import "@testing-library/jest-dom"
import { render, screen } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import ListItem from "./ListItem"

describe("ListItem component", () => {
  it("renders the restaurant image", () => {
    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        thumbnail="test-thumbnail.jpg"
      />,
    )
    const img = screen.getByRole("img")
    expect(img).toHaveAttribute("src", "test-thumbnail.jpg")
    expect(img).toHaveAttribute("width", "100")
    expect(img).toHaveAttribute("height", "100")
  })

  it("renders the address", () => {
    const address = {
      city: "Test City",
      state: "Test State",
      street: "Test Street",
      zip: "12345",
    }

    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        address={address}
        thumbnail="test-thumbnail.jpg"
      />,
    )

    const addressDiv = screen.getByText(/Test Street/i).closest("div")
    expect(addressDiv).toHaveTextContent("Test Street")
    expect(addressDiv).toHaveTextContent("Test City, Test State 12345")
  })

  it("does not render the address section when address is not provided", () => {
    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        thumbnail="test-thumbnail.jpg"
      />,
    )

    // Check that address-related text is not in the document
    expect(screen.queryByText("Test Street")).not.toBeInTheDocument()
    expect(
      screen.queryByText(/Test City, Test State 12345/),
    ).not.toBeInTheDocument()
  })

  it("renders the hours and price information", () => {
    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        thumbnail="test-thumbnail.jpg"
      />,
    )
    const hoursPriceDiv = screen.getByText(/\$\$\$/i).closest("div")
    expect(hoursPriceDiv).toHaveTextContent("$$$")
    expect(hoursPriceDiv).toHaveTextContent("Hours: M-F 10am-11pm")
  })

  it("indicates if the restaurant is open now", () => {
    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        thumbnail="test-thumbnail.jpg"
      />,
    )
    expect(screen.getByText("Open Now")).toBeInTheDocument()
  })

  it("renders the details button with correct link", () => {
    render(
      <ListItem
        slug="test-slug"
        name="Test Name"
        thumbnail="test-thumbnail.jpg"
      />,
    )
    const detailsButton = screen.getByRole("link")
    expect(detailsButton).toHaveAttribute("href", "/restaurants/test-slug")
    expect(screen.getByText("Details")).toBeInTheDocument()
  })
})

Exercise

  • Update ListItem to accept props with restaurant data.
  • Alter ListItem to return the JSX for a single item in restaurants.data, use props for the variable data.
  • Refactor RestaurantList to use ListItem to render the items in restaurants.data.

Having issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution

Click to see the solution

✏️ Update src/pages/RestaurantList/ListItem.tsx to be:

interface ListItemProps {
  address?: {
    city: string
    state: string
    street: string
    zip: string
  }
  name: string
  slug: string
  thumbnail: string
}

const ListItem: React.FC<ListItemProps> = ({
  address,
  name,
  slug,
  thumbnail,
}) => {
  return (
    <>
      <div className="restaurant">
        <img src={thumbnail} alt="" width="100" height="100" />
        <h3>{name}</h3>

        {address && (
          <div className="address">
            {address.street}
            <br />
            {address.city}, {address.state} {address.zip}
          </div>
        )}

        <div className="hours-price">
          $$$
          <br />
          Hours: M-F 10am-11pm
          <span className="open-now">Open Now</span>
        </div>

        <a className="btn" href={`/restaurants/${slug}`}>
          Details
        </a>
        <br />
      </div>
    </>
  )
}

export default ListItem

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

import CheeseThumbnail from "place-my-order-assets/images/2-thumbnail.jpg"
import PoutineThumbnail from "place-my-order-assets/images/4-thumbnail.jpg"
import ListItem from "./ListItem"

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

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>
        {restaurants.data ? (
          restaurants.data.map(({ _id, address, images, name, slug }) => (
            <ListItem
              key={_id}
              address={address}
              name={name}
              slug={slug}
              thumbnail={images.thumbnail}
            />
          ))
        ) : (
          <p>No restaurants.</p>
        )}
      </div>
    </>
  )
}

export default RestaurantList

Having issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Next steps

Next, let’s learn about routing to update the URL based on the view we’re looking at, and vice versa.