Building Components page

Learn about components, the core building blocks of every React application.

Overview

In this section, we will:

  • Learn the basics of creating components in React
  • Discover how components are structured
  • Review how React components are (fundamentally) functions

Objective: Create a React component

Our App component currently shows our restaurant list, but eventually we’ll want to show other page content. Let’s prepare now by moving all of the JSX from App to a new component called RestaurantList.

In our Place My Order app, we want to:

  • Create our first brand new React component (RestaurantList)
  • Move the logic from our App component to our new RestaurantList component

What are components?

So far, we have placed all of our JSX inside the App function. Notice two things about the App function:

  1. The name starts with a capital letter
  2. It returns something renderable (JSX)
function App() {
  return <main>Some page content</main>
}

In React, we call this a component. When you create a component in React, you are creating building blocks that can be composed, reordered, and reused much like HTML elements.

React makes it relatively straightforward to create new components. Let’s learn to build our own.

Component structure

Let’s start by creating a component from a commonly reused element, the button.

First, React component names must start with a capital letter, so we can call this Button. By convention component names use PascalCase when naming components, so longer component names will look like IconButton. Avoid hyphens and underscores.

Second, our component must return either null or something renderable, like JSX. The return value of our components is almost always JSX, though JavaScript primitives like string and number are also valid. Components cannot return complex types like arrays or objects.

const Button = () => {
  return <button className="button primary">Activate me</button>
}

Components are like small containers which can be reused throughout your application. The Button component above returns JSX and could then be rendered and reused by another component like App below.

const App = () => {
  return (
    <main>
      <Button />
      <Button />
      <Button />
    </main>
  )
}

React components are functions

The JSX syntax allows function components to look like HTML, but underneath they are still functions. The return of each component is unique and you can use the same component multiple times.

You can think of components as fancy functions.

While you can't actually do the following, this is functionally similar to what React is doing for you.

import type React from "react"

const App: React.FC = () => {
  return (
    <main>
      {Button()}
      {Button()}
      {Button()}
    </main>
  )
}

Did you notice the React.FC that was used in the previous example to type the App const? Because we're using TypeScript with our project, we can apply types to help make sure the function component is properly formed. React provides the type FC (an abbreviation for "function component") that can be applied to a function component. This type defines the arguments and return value that a function component must implement.

Setup

It’s best practice to create a new folder that will contain all of the related files for that component, including test and CSS files.

✏️ Create src/pages/ (folder)

✏️ Create src/pages/RestaurantList/ (folder)

✏️ Create src/pages/RestaurantList/index.ts and update it to be:

export { default } from "./RestaurantList"

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

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

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

export default RestaurantList

✏️ Update src/App.tsx

import CheeseThumbnail from "place-my-order-assets/images/2-thumbnail.jpg"
import PoutineThumbnail from "place-my-order-assets/images/4-thumbnail.jpg"
import RestaurantList from "./pages/RestaurantList"
import "./App.css"

function App() {
  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 App

Verify

✏️ Create src/pages/RestaurantList/RestaurantList.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 RestaurantList from "./RestaurantList"

describe("RestaurantList component", () => {
  it("renders the Restaurants header", () => {
    render(<RestaurantList />)
    expect(screen.getByText(/Restaurants/i)).toBeInTheDocument()
  })

  it("renders the restaurant images", () => {
    render(<RestaurantList />)
    const images = screen.getAllByRole("img")
    expect(images[0]).toHaveAttribute(
      "src",
      expect.stringContaining("2-thumbnail.jpg"),
    )
    expect(images[0]).toHaveAttribute("width", "100")
    expect(images[0]).toHaveAttribute("height", "100")
    expect(images[1]).toHaveAttribute(
      "src",
      expect.stringContaining("4-thumbnail.jpg"),
    )
    expect(images[1]).toHaveAttribute("width", "100")
    expect(images[1]).toHaveAttribute("height", "100")
  })

  it("renders the addresses", () => {
    render(<RestaurantList />)
    const addressDivs = screen.getAllByText(/Washburne Ave|Kinzie Street/i)
    expect(addressDivs[0]).toHaveTextContent("2451 W Washburne Ave")
    expect(addressDivs[0]).toHaveTextContent("Green Bay, WI 53295")
    expect(addressDivs[1]).toHaveTextContent("230 W Kinzie Street")
    expect(addressDivs[1]).toHaveTextContent("Green Bay, WI 53205")
  })

  it("renders the hours and price information for each restaurant", () => {
    render(<RestaurantList />)
    const hoursPriceDivs = screen.getAllByText(/\$\$\$/i)
    hoursPriceDivs.forEach((div) => {
      expect(div).toHaveTextContent("$$$")
      expect(div).toHaveTextContent("Hours: M-F 10am-11pm")
    })
  })

  it("indicates if the restaurant is open now for each restaurant", () => {
    render(<RestaurantList />)
    const openNowTags = screen.getAllByText("Open Now")
    expect(openNowTags.length).toBeGreaterThan(0)
  })

  it("renders the details buttons with correct links for each restaurant", () => {
    render(<RestaurantList />)
    const detailsButtons = screen.getAllByRole("link")
    expect(detailsButtons[0]).toHaveAttribute(
      "href",
      "/restaurants/cheese-curd-city",
    )
    expect(detailsButtons[1]).toHaveAttribute(
      "href",
      "/restaurants/poutine-palace",
    )
    detailsButtons.forEach((button) => {
      expect(button).toHaveTextContent("Details")
    })
  })
})

✏️ Update src/App.test.tsx to be a simple smoke test:

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

import App from "./App"

// Mocking RestaurantList component
vi.mock("./pages/RestaurantList", () => ({
  __esModule: true,
  default: () => <div>Mocked Restaurant List</div>,
}))

describe("App Component", () => {
  // Testing if the App component renders without crashing
  it("renders without crashing", () => {
    render(<App />)
    expect(screen.getByText("Mocked Restaurant List")).toBeInTheDocument()
  })
})

Exercise

  • Move the logic in our App component to our new RestaurantList component.
  • Update our App component to use our new RestaurantList component.

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/App.tsx

import RestaurantList from "./pages/RestaurantList"
import "./App.css"

function App() {
  return (
    <>
      <RestaurantList />
    </>
  )
}

export default App

✏️ Update src/pages/RestaurantList/RestaurantList.tsx

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

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

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

Next steps

Next we’ll learn to pass arguments to our components through props.