Building Components page

Components are the core building blocks of any React application

Overview

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

  1. Name starts with a capital letter
  2. Returns something renderable (JSX)
function App() {
  return (
    <div>
      Some page content
    </div>
  );
}

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.

Objective 1: Create a React component

Key concepts

Component structure

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

First, our component names must start with a capital letter, so we can call this Button. While its not required, the common practice is to use PascalCase when naming components so longer component names will look like IconButton.

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.

function Button() {
  return (
    <div className="button primary">
      <button>click me</button>
    </div>
  );
}

ReactDOM.render(<Button />, document.getElementById('root'));

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.

function App() {
  return (
    <div>
      <Button />
      <Button />
      <Button />
    </div>
  );
}

React components are just 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.

Components are just fancy functions. While you shouldn't do the following, you could.

function App() {
  return (
    <div>
      {Button()}
      {Button()}
      {Button()}
    </div>
  );
}

Now you're ready to create your first component.

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 = () => {
}

export default RestaurantList

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

Our App component can only show our home page content. Eventually, we'll want to show other page content. Prepare now by moving all of the JSX in App to a new component called Home.

Once the Home component is complete, add <Home /> to the JSX response of App.

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.