---
title: Bitovi Academy - Making HTTP Requests
description: Learn about how to make `fetch` requests and render requested data in React components.
---

- ![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) [ ]

# Making HTTP Requests `page`

Learn about how to make `fetch` requests and render requested data in React components.

## Overview

In this section, we will:

- Define interfaces for `useState`
- Explore the `useEffect` Hook
- Understand the effect callback function
- Utilize the dependency array
- Perform async operations inside `useEffect`
- Implement cleanup functions
- Manage environment variables
- Include query parameters in API calls
- Handle HTTP error statuses
- Catch network errors

## Objective 1: Add a `fetch` request for states in a custom Hook

So far we’ve only had hard-coded data for our states, cities, and restaurants. Let’s start loading data from an API server, beginning with the list of states!

### Defining interfaces for `useState`

When building React components, you may sometimes have local state variables that always change together, and thus would benefit by being in a single `useState()` variable together:

```
import { useState } from "react"

interface UserProfile {
  email: string
  name: string
}

const UserProfileComponent: React.FC = () => {
  const [userProfile, setUserProfile] = useState<UserProfile>({
    email: "grace.hopper@example.com",
    name: "Grace Hopper",
  })

  const updateProfile = () => {
    setUserProfile({
      email: "ada.lovelace@example.com",
      name: "Ada Lovelace",
    })
  }

  return (
    <form>
      <p>Name: {userProfile.name}</p>
      <p>Email: {userProfile.email}</p>
      <button onClick={updateProfile}>Update profile</button>
    </form>
  )
}

export default UserProfileComponent

```

In the example above, we have a `UserProfile` interface that keeps track of an `email` and `name`. We can use that interface when we call `useState()` so TypeScript is aware of the typing for the state variable and its setter.

### The `useEffect` Hook

`useEffect` is a React Hook that lets you perform side effects in your function components. It serves as a powerful tool to execute code in response to component renders or state changes.

Here is an example component with `useEffect`:

```
import { useEffect, useState } from "react"

interface Coordinates {
  latitude: number
  longitude: number
}

const GeolocationComponent: React.FC = () => {
  const [location, setLocation] = useState<Coordinates | null>(null)

  useEffect(() => {
    // Effect callback function
    navigator.geolocation.getCurrentPosition(
      (position) => {
        setLocation(position.coords)
      },
      (error) => {
        console.error(error)
      },
    )
  }, []) // Dependency array

  return (
    <main>
      {location ? (
        <p>
          Latitude: {location.latitude}, Longitude: {location.longitude}
        </p>
      ) : (
        <p>Requesting location…</p>
      )}
    </main>
  )
}

export default GeolocationComponent

```

Let’s break this example down by the two arguments that `useEffect` takes:

### Effect callback function

The first argument of `useEffect` is a function, often referred to as the “effect” function. This is where you perform your side effects, such as fetching data, setting up a subscription, or manually changing the DOM in React components.

The key aspect of this function is that it’s executed after the component renders. The effects in `useEffect` don’t block the browser from updating the screen, leading to more responsive UIs.

This effect function can optionally return another function, known as the “cleanup” function. The cleanup function is useful for performing any necessary cleanup activities when the component unmounts or before the component re-renders and the effect is re-invoked. Common examples include clearing timers, canceling network requests, or removing event listeners.

### The dependency array

The second argument of `useEffect` is an array, called the “dependency array”, which determines when your effect function should be called. The behavior of the effect changes based on the contents of this array:

Consider three scenarios based on the dependency array:

#### Empty dependency array (`[]`)

If the dependency array is an empty array, the effect runs once after the initial render.

```
import { useEffect, useState } from "react"

interface Coordinates {
  latitude: number
  longitude: number
}

const GeolocationComponent: React.FC = () => {
  const [location, setLocation] = useState<Coordinates | null>(null)

  useEffect(() => {
    // Effect callback function
    navigator.geolocation.getCurrentPosition(
      (position) => {
        setLocation(position.coords)
      },
      (error) => {
        console.error(error)
      },
    )
  }, []) // Dependency array

  return (
    <main>
      {location ? (
        <p>
          Latitude: {location.latitude}, Longitude: {location.longitude}
        </p>
      ) : (
        <p>Requesting location…</p>
      )}
    </main>
  )
}

export default GeolocationComponent

```

#### Array with values

When you include values (variables, props, state) in the dependency array, the effect will only re-run if those specific values change between renders. This selective execution can optimize performance by avoiding unnecessary work.

```
import { useEffect, useState } from "react"

function NameStorage() {
  const [name, setName] = useState("")

  useEffect(() => {
    localStorage.setItem("name", name)
  }, [name])

  return (
    <label>
      Name
      <input
        onChange={(event) => setName(event.target.value)}
        type="text"
        value={name}
      />
    </label>
  )
}

export default NameStorage

```

#### No dependency array

If the dependency array is omitted, the effect runs after every render of the component. This should not be needed.

```
import { useEffect, useState } from "react"

function UpdateLogger() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.info("Component updated!")
  }) // No dependency array, runs on every update

  return <button onClick={() => setCount(count + 1)}>Increment</button>
}

export default UpdateLogger

```

### Async operations inside useEffect

You can use APIs that return a `Promise` normally within a `useEffect`:

```
import { useEffect, useState } from "react"

function DataFetcher() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch("https://api.example.com/data")
      .then((response) => {
        const parsedData = await response.json()
        setData(parsedData)
      })
      .catch((error) => {
        // Error should be shown to the user
        console.error("Error fetching data:", error)
      })
  }, [])

  return <p>{data}</p>
}

export default DataFetcher

```

However, unlike traditional functions, `useEffect` functions can’t be marked as async. This is because returning a `Promise` from `useEffect` would conflict with its mechanism, which expects either nothing or a clean-up function to be returned.

To handle asynchronous operations, you typically define an `async` function inside the effect and then call it:

```
import { useEffect, useState } from "react"

function DataFetcher() {
  const [data, setData] = useState(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("https://api.example.com/data")
        const parsedData = await response.json()
        setData(parsedData)
      } catch (error) {
        // Error should be shown to the user
        console.error("Error fetching data:", error)
      }
    }

    fetchData()
  }, [])

  return <p>{data}</p>
}

export default DataFetcher

```

When using async/await, error handling is typically done using try-catch blocks. This allows you to gracefully handle any errors that occur during the execution of your async operation.

In this example, if `fetch` throws an error, the `catch` block catches and handles it. This pattern is crucial to prevent unhandled promise rejections and ensure that your application can respond appropriately to failures in asynchronous tasks.

### Cleanup functions

The effect function can optionally return another function, known as the “cleanup” function. The cleanup function is useful for performing any necessary cleanup activities when the component unmounts or before the component re-renders and the effect is re-invoked. Common examples include clearing timers, canceling network requests, or removing event listeners.

```
import { useEffect, useState } from "react"

function WebSocketComponent() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    const socket = new WebSocket("wss://chat.donejs.com/")

    socket.onmessage = (event) => {
      setMessages((previousMessages) => {
        return [...previousMessages, event.data]
      })
    }

    return () => {
      // Clean up (tear down) the socket connection
      return socket.close()
    }
  }, [])

  return (
    <ol>
      {messages.map((message) => (
        <li key={message}>{message}</li>
      ))}
    </ol>
  )
}

export default WebSocketComponent

```

In the example above, we’re creating a WebSocket connection to an API when the component is first rendered (note the empty dependency array).

When the component is removed from the DOM, the cleanup function will run and tear down the WebSocket connection.

### Environment variables

The way we’re accessing our locally run API during development may be different than how we access it in production. To prepare for this, we’ll set an environment variable to do what we need.

Environment variables are dynamic-named values that can affect the way running processes on a computer will behave. In the context of software development, they are used to manage specific settings or configurations that should not be hardcoded within the application’s source code.

This is particularly useful for:

- **Security:** Keeping sensitive data like API keys or database passwords out of the source code.
- **Flexibility:** Allowing configurations to change depending on the environment (development, staging, production).
- **Convenience:** Making it easier to update configuration settings without changing the application’s code.

In our project, we’ll utilize environment variables to set ourselves up to be able to differentiate between the development and production environments, especially in how we connect to different instances of our API.

#### Using environment variables with Vite

Vite, our build tool, provides an easy way to work with environment variables. In Vite, any environment variable prefixed with `VITE_` is made available in our client-side source code. This prefix is necessary because Vite only exposes variables that are explicitly meant for client-side consumption, ensuring server-side variables remain secure.

Here’s how we can use it: in our project’s root directory, we can create a `.env` file with variables like this:

```
VITE_PMO_API = '//localhost:7070'
```

Note: if you are using CodeSandbox or StackBlitz, `.env` has already been created with a remote connection to the PMO API at `https://www.place-my-order.com/`. Leave it as is instead of changing to `localhost`, because these container services make it difficult to run the API alongside the Web server.

Then we can access this variable using `import.meta.env`:

```
const response = await fetch(`${import.meta.env.VITE_PMO_API}/data`, {
  method: "GET",
})

const data = await response.json()
```

Concatenating the two, this will be the equivalent of making this `fetch` request:

```
const response = await fetch(`//localhost:7070/data`, {
  method: "GET",
})
```

### Setup 1

#### Install the Place My Order API

Before we begin requesting data from our API, we need to install the `place-my-order-api` module, which will generate fake restaurant data and serve it from port `7070`.

✏️ Run:

```
npm install place-my-order-api@1
```

✏️ Next add an API script to your `package.json`. NOTE: if on Windows, change `/` to `\\`.

```
{
  "name": "place-my-order",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "api": "./node_modules/.bin/place-my-order-api -p 7070",
    "typecheck": "tsc",
    "eslint": "eslint vite.config.ts src",
    "prettier": "prettier --check vite.config.ts src",
    "test": "vitest",
    "dev": "vite",
    "build": "tsc && vite build"
  },
  "prettier": {
    "semi": false,
    "trailingComma": "all"
  },
  "dependencies": {
    "place-my-order-api": "^1.3.0",
    "place-my-order-assets": "^0.2.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.29.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.4.2",
    "@testing-library/react": "^14.2.1",
    "@testing-library/user-event": "^14.5.2",
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^9.13.0",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-jest": "^28.11.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-react": "^7.37.4",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "eslint-plugin-testing-library": "^7.1.1",
    "globals": "^15.15.0",
    "jsdom": "^24.0.0",
    "prettier": "^3.2.5",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.11.0",
    "vite": "^5.0.8",
    "vitest": "^1.2.2"
  }
}

```

✏️ In a **new** terminal window, start the API server by running:

```
npm run api
```

Double check the API by navigating to [localhost:7070/restaurants](http://localhost:7070/restaurants). You should see a JSON list of restaurant data.

It will be helpful to have a third terminal tab for the `npm run api` command.

#### Create and update files

✏️ Create **.env** and update it to be:

```
VITE_PMO_API = '//localhost:7070'
```

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

```
import type { State } from "../../services/pmo/restaurant"
import CheeseThumbnail from "place-my-order-assets/images/2-thumbnail.jpg"
import PoutineThumbnail from "place-my-order-assets/images/4-thumbnail.jpg"
import { useState, useCallback } from "react"
import ListItem from "./ListItem"
import { useCities, useStates } from "../../services/pmo/restaurant"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const states = [
    { name: "Illinois", short: "IL" },
    { name: "Wisconsin", short: "WI" },
  ]

  const cities = useCities(state)

  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",
      },
    ],
  }

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            City:
            {state ? (
              cities.map(({ name }) => (
                <button
                  key={name}
                  onClick={() => updateCity(name)}
                  type="button"
                >
                  {name}
                </button>
              ))
            ) : (
              <> Choose a state before selecting a city</>
            )}
            <hr />
            <p>Current city: {city || "(none)"}</p>
          </div>
        </form>

        {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

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, State } from "./interfaces"
import { useEffect, useState } from "react"

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): City[] {
  const cities = [
    { name: "Madison", state: "WI" },
    { name: "Springfield", state: "IL" },
  ]
  return cities.filter((city) => {
    return city.state === state
  })
}

export function useStates(): StatesResponse {
  // Exercise: Update the `useState` in `hooks.ts` to call `useEffect()` and `fetch` data from `${process.env.PMO_API}/states`.
}

```

### Verify 1

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

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

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

// Mock the hooks used in the component
vi.mock("../../services/pmo/restaurant", () => ({
  useCities: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
  useStates: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
}))

describe("RestaurantList component", () => {
  beforeEach(async () => {
    vi.spyOn(restaurantHooks, "useCities").mockReturnValue([
      { name: "Green Bay", state: "WI" },
      { name: "Madison", state: "WI" },
    ])
    render(<RestaurantList />)
    await act(() => {})
  })

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

  it("renders the restaurant images", () => {
    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", () => {
    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", () => {
    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", () => {
    const openNowTags = screen.getAllByText("Open Now")
    expect(openNowTags.length).toBeGreaterThan(0)
  })

  it("renders the details buttons with correct links for each restaurant", () => {
    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")
    })
  })

  it("renders ListItem components for each restaurant", () => {
    const restaurantNames = screen.getAllByText(
      /Cheese Curd City|Poutine Palace/,
    )
    expect(restaurantNames.length).toBe(2)
  })
})

```

✏️ Update **src/services/pmo/restaurant/hooks.test.ts** to be:

```
import { renderHook } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { useCities, useStates } from "./hooks"

describe("useCities Hook", () => {
  it("should return cities from Wisconsin when state is WI", () => {
    const { result } = renderHook(() => useCities("WI"))
    expect(result.current).toHaveLength(1)
    expect(result.current[0].name).toBe("Madison")
  })

  it("should return cities from Illinois when state is IL", () => {
    const { result } = renderHook(() => useCities("IL"))
    expect(result.current).toHaveLength(1)
    expect(result.current[0].name).toBe("Springfield")
  })

  it("should return no cities for an unknown state", () => {
    const { result } = renderHook(() => useCities("CA"))
    expect(result.current).toHaveLength(0)
  })
})

describe("useStates Hook", () => {
  beforeEach(async () => {
    // Mocking the fetch function
    // @ts-ignore `global` exists in node environments like our test runner.
    global.fetch = vi.fn()
  })

  it("should set the states data on successful fetch", async () => {
    const mockStates = [{ name: "State1" }, { name: "State2" }]
    ;(fetch as Mock).mockResolvedValueOnce({
      json: () => Promise.resolve({ data: mockStates }),
    })

    const { result } = renderHook(() => useStates())

    await waitFor(() => {
      expect(result.current.isPending).toBe(false)
      expect(result.current.data).toEqual(mockStates)
      expect(result.current.error).toBeUndefined()
    })
  })
})

```

### Exercise 1

- Update the `useStates` in `hooks.ts` to call `useEffect()` and `fetch` data from `${process.env.PMO_API}/states`.
- Update `StateList.tsx` to call `useStates()` and use the `StateResponse` interface.

**Hint:** Call your state setter after you parse the JSON response from `fetch()`.

**Hint:** `useStates()` return `isPending` and `error`. Use these states to inform the user the status.

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

### Solution 1

Click to see the solution

✏️ 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 { useState, useCallback } from "react"
import ListItem from "./ListItem"
import { useCities, useStates } from "../../services/pmo/restaurant"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const statesResponse = useStates()

  const cities = useCities(state)

  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",
      },
    ],
  }

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            City:
            {state ? (
              cities.map(({ name }) => (
                <button
                  key={name}
                  onClick={() => updateCity(name)}
                  type="button"
                >
                  {name}
                </button>
              ))
            ) : (
              <> Choose a state before selecting a city</>
            )}
            <hr />
            <p>Current city: {city || "(none)"}</p>
          </div>
        </form>

        {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

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, State } from "./interfaces"
import { useEffect, useState } from "react"

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): City[] {
  const cities = [
    { name: "Madison", state: "WI" },
    { name: "Springfield", state: "IL" },
  ]
  return cities.filter((city) => {
    return city.state === state
  })
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${import.meta.env.VITE_PMO_API}/states`, {
        method: "GET",
      })

      const data = await response.json()

      setResponse({
        data: data?.data || undefined,
        error: undefined,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

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

## Objective 2: Update the `useCities` Hook to fetch data from the API

Let’s continue our quest to load data from our API and update our `useCities` Hook to fetch data.

To do this, we’ll need to include query parameters in our API call to the `/cities` endpoint.

### Including query parameters in API calls

Query parameters are a defined set of parameters attached to the end of a URL. They are used to define and pass data in the form of key-value pairs. The parameters are separated from the URL itself by a `?` symbol, and individual key-value pairs are separated by the `&` symbol.

A basic URL with query parameters looks like this: `http://www.example.com/page?param1=value1&param2=value2`

Here’s a breakdown of this URL:

- Base URL: `http://www.example.com/page`
- Query Parameter Indicator: `?`
- Query Parameters: 
    - `param1=value1`
    - `param2=value2`

### Setup 2

✏️ 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 { useState, useCallback } from "react"
import { useCities, useStates } from "../../services/pmo/restaurant"
import ListItem from "./ListItem"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const statesResponse = useStates()

  const cities = useCities(state)

  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",
      },
    ],
  }

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  const chooseCityPlaceholder = useCallback(()=>{
    let placeholderText = "Choose a city";

    if(!state) {
      placeholderText = "Choose a state before selecting a city";
    } else if (citiesResponse.isPending) {
      placeholderText = "Loading cities…";
    } else if (citiesResponse.error) {
      placeholderText = citiesResponse.error.message;
    } 

    return placeholderText;
  }, [state, citiesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            <label className="control-label" htmlFor="citySelect">
              City
            </label>
            <select
              className="form-control"
              id="citySelect"
              onChange={(event) => updateCity(event.target.value)}
              value={city}
            >
              <option key="choose_city" value="">
                {chooseCityPlaceholder()}
              </option>
              {state &&
                citiesResponse.data?.map(({ name }) => (
                  <option key={name} value={name}>
                    {name}
                  </option>
                ))}
            </select>
          </div>
        </form>

        {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

```

### Verify 2

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

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

import RestaurantList from "./RestaurantList"

import { useCities, useStates } from "../../services/pmo/restaurant"

// Mock the hooks used in the component
vi.mock("../../services/pmo/restaurant", () => ({
  useCities: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
  useStates: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
}))

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

  it("renders state and city dropdowns", async () => {
    render(<RestaurantList />)
    await act(() => {})
    expect(screen.getByLabelText(/State/i)).toBeInTheDocument()
    expect(screen.getByLabelText(/City/i)).toBeInTheDocument()
  })

  it("renders correctly with initial states", async () => {
    useStates.mockReturnValue({ data: undefined, isPending: true, error: undefined })
    useCities.mockReturnValue({ data: undefined, isPending: false, error: undefined })

    render(<RestaurantList />)
    await act(() => {})

    expect(screen.getByText(/Restaurants/)).toBeInTheDocument()
    expect(screen.getByText(/Loading states…/)).toBeInTheDocument()
  })

  it("displays error messages correctly", async () => {
    useStates.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { message: "Error loading states" },
    })
    useCities.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { message: "Error loading cities" },
    })

    render(<RestaurantList />)
    await act(() => {})

    expect(screen.getByText(/Error loading states/)).toBeInTheDocument()
  })

  it("renders restaurants correctly", async () => {
    useStates.mockReturnValue({
      data: [{ short: "CA", name: "California" }],
      isPending: false,
      error: undefined,
    })
    useCities.mockReturnValue({
      data: [{ name: "Los Angeles" }],
      isPending: false,
      error: undefined,
    })

    render(<RestaurantList />)
    await act(() => {})

    await userEvent.selectOptions(screen.getByLabelText(/State/), "CA")
    await userEvent.selectOptions(screen.getByLabelText(/City/), "Los Angeles")

    expect(screen.getByText("Cheese Curd City")).toBeInTheDocument()
  })
})

```

✏️ Update **src/services/pmo/restaurant/hooks.test.ts** to be:

```
import { renderHook, waitFor } from "@testing-library/react"
import { beforeEach, describe, expect, it, Mock, vi } from "vitest"
import { useCities, useStates } from "./hooks"

describe("useCities Hook", () => {
  beforeEach(() => {
    // @ts-ignore `global` exists in node environments like our test runner.
    global.fetch = vi.fn()
  })

  it("initial state of useCities", async () => {
    vi.mocked(fetch).mockResolvedValueOnce({
      json: async () => ({ data: null }),
    })

    const { result } = renderHook(() => useCities("someState"))
    await waitFor(() => {
      expect(result.current.isPending).toBe(true)
      expect(result.current.data).toBeUndefined()
      expect(result.current.error).toBeUndefined()
    })
  })

  it("fetches cities successfully", async () => {
    const mockCities = [
      { id: 1, name: "City1" },
      { id: 2, name: "City2" },
    ]
    vi.mocked(fetch).mockResolvedValueOnce({
      json: async () => ({ data: mockCities }),
    })

    const { result } = renderHook(() => useCities("someState"))

    await waitFor(() => {
      expect(result.current.data).toEqual(mockCities)
      expect(result.current.isPending).toBe(false)
      expect(result.current.error).toBeUndefined()
    })
  })
})

describe("useStates Hook", () => {
  beforeEach(async () => {
    // Mocking the fetch function
    // @ts-ignore `global` exists in node environments like our test runner.
    global.fetch = vi.fn()
  })

  it("should set the states data on successful fetch", async () => {
    const mockStates = [{ name: "State1" }, { name: "State2" }]
    ;(fetch as Mock).mockResolvedValueOnce({
      json: () => Promise.resolve({ data: mockStates }),
    })

    const { result } = renderHook(() => useStates())

    await waitFor(() => {
      expect(result.current.isPending).toBe(false)
      expect(result.current.data).toEqual(mockStates)
      expect(result.current.error).toBeUndefined()
    })
  })
})

```

### Exercise 2

Update our `useCities()` Hook to fetch cities from the Place My Order API, given a selected state.

When calling the Place My Order API, include the `state` query parameter: `http://localhost:7070/cities?state=MO`

Hint: Remember to include the `state` in the dependency array of the `useEffect()` in `useCities()`.

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/02-problem?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/02-problem?file=src/services/pmo/restaurant/hooks.ts) to do this exercise in an online code editor.

### Solution 2

Click to see the solution

✏️ 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 { useState, useCallback } from "react"
import { useCities, useStates } from "../../services/pmo/restaurant"
import ListItem from "./ListItem"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const statesResponse = useStates()

  const citiesResponse = useCities(state)

  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",
      },
    ],
  }

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  const chooseCityPlaceholder = useCallback(()=>{
    let placeholderText = "Choose a city";

    if(!state) {
      placeholderText = "Choose a state before selecting a city";
    } else if (citiesResponse.isPending) {
      placeholderText = "Loading cities…";
    } else if (citiesResponse.error) {
      placeholderText = citiesResponse.error.message;
    } 

    return placeholderText;
  }, [state, citiesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            <label className="control-label" htmlFor="citySelect">
              City
            </label>
            <select
              className="form-control"
              id="citySelect"
              onChange={(event) => updateCity(event.target.value)}
              value={city}
            >
              <option key="choose_city" value="">
                {chooseCityPlaceholder()}
              </option>
              {state &&
                citiesResponse.data?.map(({ name }) => (
                  <option key={name} value={name}>
                    {name}
                  </option>
                ))}
            </select>
          </div>
        </form>

        {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

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, State } from "./interfaces"
import { useEffect, useState } from "react"

interface CitiesResponse {
  data: City[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `${import.meta.env.VITE_PMO_API}/cities?state=${state}`,
        {
          method: "GET",
        },
      )

      const data = await response.json()

      setResponse({
        data: data?.data || undefined,
        error: undefined,
        isPending: false,
      })
    }
    fetchData()
  }, [state])

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${import.meta.env.VITE_PMO_API}/states`, {
        method: "GET",
      })

      const data = await response.json()

      setResponse({
        data: data?.data || undefined,
        error: undefined,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/02-solution?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/02-solution?file=src/services/pmo/restaurant/hooks.ts).

## Objective 3: Create an `apiRequest` helper and use it in the Hooks.

Now that we have two Hooks that fetch data in a similar way, let’s create an `apiRequest` helper function that both Hooks can use.

### Handle HTTP error statuses

When you make a request with the Fetch API, it does not reject on HTTP error statuses (like `404` or `500`). Instead, it resolves normally (with an `ok` status set to `false`), and it only rejects on network failure or if anything prevented the request from completing.

Here’s the API that `fetch` provides to handle these HTTP errors:

- `.ok`: This is a shorthand property that returns `true` if the response’s status code is in the range `200`-`299`, indicating a successful request.
- `.status`: This property returns the status code of the response (e.g. `200` for success, `404` for `Not Found`, etc.).
- `.statusText`: This provides the status message corresponding to the status code (e.g. `'OK'`, `'Not Found'`, etc.).

```
const response = await fetch("https://api.example.com/data", {
  method: "GET",
})

const data = await response.json()
const error = response.ok
  ? undefined
  : new Error(`${response.status} (${response.statusText})`)

```

In the example above, we check the `response.ok` property to see if the status code is in the `200`-`299` (successful) range. If not, we create an `error` object that contains the status code and text (e.g. `404 Not Found`).

### Catch network errors

Network errors occur when there is a problem in completing the request, like when the user is offline, the server is unreachable, or there is a DNS lookup failure.

In these cases, the `fetch` API will *not* resolve with data, but instead it will throw an error that needs to be caught.

Let’s take a look at how to handle these types of errors:

```
try {
  const response = await fetch("https://api.example.com/data", {
    method: "GET",
  })

  const data = await response.json()
  const error = response.ok
    ? undefined
    : new Error(`${response.status} (${response.statusText})`)
  // Do something with data and error
} catch (error) {
  const parsedError =
    error instanceof Error ? error : new Error("An unknown error occurred")
  // Do something with parsedError
}

```

In the example above, we `catch` the `error` and check its type. If it’s already an `instanceof Error`, then it will have a `message` property and we can use it as-is. If it’s not, then we can create our own `new Error()` so we *always* have an error to consume in our Hooks or components.

### Setup 3

✏️ Create **src/services/pmo/api/api.ts** and update it to be:

```
export async function apiRequest<Data = never, Params = unknown>({
  method,
  params,
  path,
}: {
  method: string
  params?: Params
  path: string
}): Promise<{ data?: Data; error?: Error }> {
  
  // Exercise: Implement the `apiRequest` helper function to handle errors returned and thrown from `fetch()`.

  return {
    data: undefined,
    error: undefined,
  }
}

export function stringifyQuery(input: Record<string, string | null | undefined>): string {
  const output: string[] = []

  for (const [key, value] of Object.entries(input)) {
    if (typeof value !== "undefined" && value !== null) {
      output.push(`${key}=${value}`)
    }
  }

  return output.join("&")
}

```

✏️ Create **src/services/pmo/api/index.ts** and update it to be:

```
export * from "./api"

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, State } from "./interfaces"
import { useEffect, useState } from "react"
import { apiRequest } from "../api"

interface CitiesResponse {
  data: City[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(
        `${import.meta.env.VITE_PMO_API}/cities?state=${state}`,
        {
          method: "GET",
        },
      )

      const data = await response.json()

      setResponse({
        data: data?.data || undefined,
        error: undefined,
        isPending: false,
      })
    }
    fetchData()
  }, [state])

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${import.meta.env.VITE_PMO_API}/states`, {
        method: "GET",
      })

      const data = await response.json()

      setResponse({
        data: data?.data || undefined,
        error: undefined,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

### Verify 3

✏️ Create **src/services/pmo/api/api.test.ts** and update it to be:

```
import { apiRequest, stringifyQuery } from "./api"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

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

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

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

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

describe("apiRequest function", () => {
  it("should handle a successful request", async () => {
    // Mock the fetch response
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ message: "success" }),
      statusText: "OK",
      status: 200,
    })

    const response = await apiRequest({
      method: "GET",
      path: "/test",
    })

    expect(response).toEqual({ data: { message: "success" }, error: undefined })
    expect(mockFetch).toHaveBeenCalledWith(
      `${import.meta.env.VITE_PMO_API}/test?`,
      { method: "GET" },
    )
  })

  it("should handle a failed request", async () => {
    // Mock the fetch response
    mockFetch.mockResolvedValueOnce({
      ok: false,
      json: () => Promise.resolve({ message: "error" }),
      statusText: "Bad Request",
      status: 400,
    })

    const response = await apiRequest({
      method: "GET",
      path: "/test",
    })

    expect(response).toEqual({
      data: { message: "error" },
      error: new Error("400 (Bad Request)"),
    })
  })

  it("should handle network errors", async () => {
    // Mock a network error
    mockFetch.mockRejectedValueOnce(new Error("Network Error"))

    const response = await apiRequest({
      method: "GET",
      path: "/test",
    })

    expect(response).toEqual({ data: undefined, error: new Error("Network Error") })
  })
})

describe("stringifyQuery function", () => {
  it("should correctly stringify query parameters", () => {
    const query = stringifyQuery({ foo: "bar", baz: "qux" })
    expect(query).toBe("foo=bar&baz=qux")
  })

  it("should omit undefined and null values", () => {
    const query = stringifyQuery({ foo: "bar", baz: null, qux: undefined })
    expect(query).toBe("foo=bar")
  })
})

```

✏️ Update **src/services/pmo/restaurant/hooks.test.ts** to be:

```
import { renderHook, waitFor } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import type { ArgumentsType, Mock } from "vitest"
import { apiRequest } from "../api"
import { useCities, useStates } from "./hooks"

// Mock the apiRequest function
vi.mock("../api", () => ({
  apiRequest: vi.fn(),
}))

const mockApiRequest = apiRequest as Mock<ArgumentsType<typeof apiRequest>, ReturnType<typeof apiRequest>>;

describe("Hooks", () => {
  beforeEach(() => {
    vi.resetAllMocks()
  })

  describe("useCities hook", () => {
    it("should return cities data successfully", async () => {
      const mockCities = [
        { id: 1, name: "City1" },
        { id: 2, name: "City2" },
      ]
      mockApiRequest.mockResolvedValue({ data: mockCities , error: undefined })

      const { result } = renderHook(() => useCities("test-state"))

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toEqual(mockCities)
        expect(result.current.error).toBeUndefined()
      })
    })

    it("should handle error when fetching cities data", async () => {
      const mockError = new Error("Error fetching cities")
      mockApiRequest.mockResolvedValue({ data: undefined, error: mockError })

      const { result } = renderHook(() => useCities("test-state"))

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toBeUndefined()
        expect(result.current.error).toEqual(mockError)
      })
    })
  })

  describe("useStates hook", () => {
    it("should return states data successfully", async () => {
      const mockStates = [
        { id: 1, name: "State1" },
        { id: 2, name: "State2" },
      ]
      mockApiRequest.mockResolvedValue({ data: mockStates , error: undefined })

      const { result } = renderHook(() => useStates())

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toEqual(mockStates)
        expect(result.current.error).toBeUndefined()
      })
    })

    it("should handle error when fetching states data", async () => {
      const mockError = new Error("Error fetching states")
      mockApiRequest.mockResolvedValue({ data: undefined, error: mockError })

      const { result } = renderHook(() => useStates())

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toBeUndefined()
        expect(result.current.error).toEqual(mockError)
      })
    })
  })
})

```

### Exercise 3

- Implement the `apiRequest` helper function to handle errors returned and thrown from `fetch()`.
- Update the `useCities` and `useStates` Hooks to use the `data` and `error` returned from `apiRequest`.

Hint: Use the new `stringifyQuery` function to convert an object of query parameters to a string:

```
stringifyQuery({
  param1: "value1",
  param2: "value2",
})
```

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/03-problem?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/03-problem?file=src/services/pmo/restaurant/hooks.ts) to do this exercise in an online code editor.

### Solution 3

Click to see the solution

✏️ Update **src/services/pmo/api/api.ts** to be:

```

export async function apiRequest<Data = never, Params = unknown>({
  method,
  params,
  path,
}: {
  method: string
  params?: Params
  path: string
}): Promise<{ data?: Data; error?: Error }> {
  try {
    const query = params ? stringifyQuery(params) : ""
    const response = await fetch(
      `${import.meta.env.VITE_PMO_API}${path}?${query}`,
      {
        method,
      },
    )

    const data = await response.json()
    const error = response.ok
      ? undefined
      : new Error(`${response.status} (${response.statusText})`)

    return {
      data: "data" in data ? data.data : data,
      error: error,
    }
  } catch (error) {
    return {
      data: undefined,
      error:
        error instanceof Error ? error : new Error("An unknown error occurred"),
    }
  }
}

export function stringifyQuery(input: Record<string, string | null | undefined>): string {
  const output: string[] = []

  for (const [key, value] of Object.entries(input)) {
    if (typeof value !== "undefined" && value !== null) {
      output.push(`${key}=${value}`)
    }
  }

  return output.join("&")
}

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, State } from "./interfaces"
import { useEffect, useState } from "react"
import { apiRequest } from "../api"

interface CitiesResponse {
  data: City[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<City[]>({
        method: "GET",
        path: "/cities",
        params: {
          state: state,
        },
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [state])

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<State[]>({
        method: "GET",
        path: "/states",
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/03-solution?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/03-solution?file=src/services/pmo/restaurant/hooks.ts).

## Objective 4: Fetch the list of restaurants

Let’s finish our quest to load data from our API by creating a Hook to fetch the list of restaurants and use it in our component.

Now that we are able to capture a user’s state and city preferences, we want to only show the restaurants in the selected city:

![A web page titled “Restaurants” from place-my-order.com showing that “Illinois” and “Chicago” are selected. Underneath the dropdowns is a list of restaurants that are in Chicago.](https://bitovi.github.io/academy/static/img/react-vite/09-making-http-requests/04-solution-with-selection.png)

### Setup 4

✏️ 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 { useState, useCallback } from "react"
import {
  useCities,
  useRestaurants,
  useStates,
} from "../../services/pmo/restaurant"
import ListItem from "./ListItem"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const statesResponse = useStates()

  const citiesResponse = useCities(state)

  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",
      },
    ],
  }

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  const chooseCityPlaceholder = useCallback(()=>{
    let placeholderText = "Choose a city";

    if(!state) {
      placeholderText = "Choose a state before selecting a city";
    } else if (citiesResponse.isPending) {
      placeholderText = "Loading cities…";
    } else if (citiesResponse.error) {
      placeholderText = citiesResponse.error.message;
    } 

    return placeholderText;
  }, [state, citiesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            <label className="control-label" htmlFor="citySelect">
              City
            </label>
            <select
              className="form-control"
              id="citySelect"
              onChange={(event) => updateCity(event.target.value)}
              value={city}
            >
              <option key="choose_city" value="">
                {chooseCityPlaceholder()}
              </option>
              {state &&
                citiesResponse.data?.map(({ name }) => (
                  <option key={name} value={name}>
                    {name}
                  </option>
                ))}
            </select>
          </div>
        </form>

        {city && restaurantsResponse.error && (
          <p aria-live="polite" className="restaurant">
            Error loading restaurants: {restaurantsResponse.error.message}
          </p>
        )}

        {city && restaurantsResponse.isPending && (
          <p aria-live="polite" className="restaurant loading">
            Loading restaurants…
          </p>
        )}

        {city &&
          restaurantsResponse.data &&
          (restaurantsResponse.data.length === 0
            ? !restaurantsResponse.isPending && (
                <p aria-live="polite">No restaurants found.</p>
              )
            : restaurantsResponse.data.map(
                ({ _id, slug, name, address, images }) => (
                  <ListItem
                    key={_id}
                    address={address}
                    name={name}
                    slug={slug}
                    thumbnail={images.thumbnail}
                  />
                ),
              ))}
      </div>
    </>
  )
}

export default RestaurantList

```

✏️ Update **src/services/pmo/restaurant/interfaces.ts** to be:

```
export interface City {
  name: string
  state: string
}

interface Item {
  name: string
  price: number
}

interface Menu {
  dinner: Item[]
  lunch: Item[]
}

interface Address {
  city: string
  state: string
  street: string
  zip: string
}

interface Images {
  banner: string
  owner: string
  thumbnail: string
}

export interface Restaurant {
  _id: string
  address?: Address
  images: Images
  menu: Menu
  name: string
  slug: string
}

export interface State {
  name: string
  short: string
}

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, Restaurant, State } from "./interfaces"
import { useEffect, useState } from "react"
import { apiRequest } from "../api"

interface CitiesResponse {
  data: City[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<City[]>({
        method: "GET",
        path: "/cities",
        params: {
          state: state,
        },
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [state])

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<State[]>({
        method: "GET",
        path: "/states",
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

### Verify 4

If you’ve implemented the solution correctly, when you use the select boxes to choose state and city, you should see a list of just restaurants from the selected city returned.

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

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

import RestaurantList from "./RestaurantList"

import {
  useCities,
  useRestaurants,
  useStates,
} from "../../services/pmo/restaurant"

// Mock the hooks used in the component
vi.mock("../../services/pmo/restaurant", () => ({
  useCities: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
  useRestaurants: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
  useStates: vi.fn(() => {
    return {
      data: undefined,
      error: undefined,
      isPending: false,
    }
  }),
}))

const mockUseCities = useCities as Mock<ArgumentsType<typeof useCities>, ReturnType<typeof useCities>>
const mockUseStates = useStates as Mock<ArgumentsType<typeof useStates>, ReturnType<typeof useStates>>
const mockUseRestaurants = useRestaurants as Mock<ArgumentsType<typeof useRestaurants>, ReturnType<typeof useRestaurants>>

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

  it("renders state and city dropdowns", () => {
    render(<RestaurantList />)
    expect(screen.getByLabelText(/State/i)).toBeInTheDocument()
    expect(screen.getByLabelText(/City/i)).toBeInTheDocument()
  })

  it("renders correctly with initial states", () => {
    mockUseStates.mockReturnValue({ data: undefined, isPending: true, error: undefined })
    mockUseCities.mockReturnValue({ data: undefined, isPending: false, error: undefined })
    mockUseRestaurants.mockReturnValue({
      data: undefined,
      isPending: false,
      error: undefined,
    })

    render(<RestaurantList />)

    expect(screen.getByText(/Restaurants/)).toBeInTheDocument()
    expect(screen.getByText(/Loading states…/)).toBeInTheDocument()
  })

  it("displays error messages correctly", () => {
    mockUseStates.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: 'loading-error', message: "Error loading states" },
    })
    mockUseCities.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: 'loading-error', message: "Error loading cities" },
    })
    mockUseRestaurants.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: 'loading-error', message: "Error loading restaurants" },
    })

    render(<RestaurantList />)

    expect(screen.getByText(/Error loading states/)).toBeInTheDocument()
  })

  it("renders restaurants correctly", async () => {
    mockUseStates.mockReturnValue({
      data: [{ short: "CA", name: "California" }],
      isPending: false,
      error: undefined,
    })
    mockUseCities.mockReturnValue({
      data: [{ name: "Los Angeles", state: 'CA' }],
      isPending: false,
      error: undefined,
    })
    mockUseRestaurants.mockReturnValue({
      data: [
        {
          _id: "1",
          slug: "test-restaurant",
          name: "Test Restaurant",
          address: { street: "123 Test St", city: "Anytown", "state": "USA", zip: "12345" },
          images: { thumbnail: "test.jpg", banner: "", owner: "" },
          menu: { dinner: [], lunch: [] }
        },
      ],
      isPending: false,
      error: undefined,
    })

    render(<RestaurantList />)

    await userEvent.selectOptions(screen.getByLabelText(/State/), "CA")
    await userEvent.selectOptions(screen.getByLabelText(/City/), "Los Angeles")

    expect(screen.getByText("Test Restaurant")).toBeInTheDocument()
  })
})

```

✏️ Update **src/services/pmo/restaurant/hooks.test.ts** to be:

```
import { renderHook, waitFor } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import type { ArgumentsType, Mock } from "vitest"
import { apiRequest } from "../api"
import { useCities, useRestaurants, useStates } from "./hooks"

// Mock the apiRequest function
vi.mock("../api", () => ({
  apiRequest: vi.fn(),
}))

const mockApiRequest = apiRequest as Mock<ArgumentsType<typeof apiRequest>, ReturnType<typeof apiRequest>>

describe("Hooks", () => {
  beforeEach(() => {
    vi.resetAllMocks()
  })

  describe("useCities hook", () => {
    it("should return cities data successfully", async () => {
      const mockCities = [
        { id: 1, name: "City1" },
        { id: 2, name: "City2" },
      ]
      mockApiRequest.mockResolvedValue({ data: mockCities , error: undefined })

      const { result } = renderHook(() => useCities("test-state"))

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toEqual(mockCities)
        expect(result.current.error).toBeUndefined()
      })
    })

    it("should handle error when fetching cities data", async () => {
      const mockError = new Error("Error fetching cities")
      mockApiRequest.mockResolvedValue({ data: undefined, error: mockError })

      const { result } = renderHook(() => useCities("test-state"))

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toBeUndefined()
        expect(result.current.error).toEqual(mockError)
      })
    })
  })

  describe("useRestaurants hook", () => {
    it("should return restaurants data successfully", async () => {
      const mockRestaurants = [
        { id: 1, name: "Restaurant1" },
        { id: 2, name: "Restaurant2" },
      ]
      mockApiRequest.mockResolvedValue({
        data: mockRestaurants,
        error: undefined,
      })

      const { result } = renderHook(() =>
        useRestaurants("test-state", "test-city"),
      )

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toEqual(mockRestaurants)
        expect(result.current.error).toBeUndefined()
      })
    })

    it("should handle error when fetching restaurants data", async () => {
      const mockError = new Error("Error fetching restaurants")
      mockApiRequest.mockResolvedValue({ data: undefined, error: mockError })

      const { result } = renderHook(() =>
        useRestaurants("test-state", "test-city"),
      )

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toBeUndefined()
        expect(result.current.error).toEqual(mockError)
      })
    })
  })

  describe("useStates hook", () => {
    it("should return states data successfully", async () => {
      const mockStates = [
        { id: 1, name: "State1" },
        { id: 2, name: "State2" },
      ]
      mockApiRequest.mockResolvedValue({ data: mockStates , error: undefined })

      const { result } = renderHook(() => useStates())

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toEqual(mockStates)
        expect(result.current.error).toBeUndefined()
      })
    })

    it("should handle error when fetching states data", async () => {
      const mockError = new Error("Error fetching states")
      mockApiRequest.mockResolvedValue({ data: undefined, error: mockError })

      const { result } = renderHook(() => useStates())

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy()
        expect(result.current.data).toBeUndefined()
        expect(result.current.error).toEqual(mockError)
      })
    })
  })
})

```

### Exercise 4

- Create a `useRestaurants` Hook for fetching the list of restaurants.
- Update `RestaurantList.tsx` to use our new `useRestaurants` Hook.

Hint: The requested URL with query parameters should look like this: `'/restaurants?filter[address.state]=IL&filter[address.city]=Chicago'`

**Having issues with your local setup?** You can use either [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/04-problem?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/04-problem?file=src/services/pmo/restaurant/hooks.ts) to do this exercise in an online code editor.

### Solution 4

Click to see the solution

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

```
import { useState, useCallback } from "react"
import {
  useCities,
  useRestaurants,
  useStates,
} from "../../services/pmo/restaurant"
import ListItem from "./ListItem"

const RestaurantList: React.FC = () => {
  const [state, setState] = useState("")
  const [city, setCity] = useState("")

  const statesResponse = useStates()

  const citiesResponse = useCities(state)

  const restaurantsResponse = useRestaurants(state, city)

  const updateState = (stateShortCode: string) => {
    setState(stateShortCode)
    setCity("")
  }

  const updateCity = (cityName: string) => {
    setCity(cityName)
  }

  const chooseStatePlaceholder = useCallback(()=>{
    let placeholderText = "Choose a state";

    if(statesResponse.isPending) {
      placeholderText = "Loading states…";
    } else if (statesResponse.error) {
      placeholderText = statesResponse.error.message
    }

    return placeholderText;
  }, [statesResponse])

  const chooseCityPlaceholder = useCallback(()=>{
    let placeholderText = "Choose a city";

    if(!state) {
      placeholderText = "Choose a state before selecting a city";
    } else if (citiesResponse.isPending) {
      placeholderText = "Loading cities…";
    } else if (citiesResponse.error) {
      placeholderText = citiesResponse.error.message;
    } 

    return placeholderText;
  }, [state, citiesResponse])

  return (
    <>
      <div className="restaurants">
        <h2 className="page-header">Restaurants</h2>

        <form className="form">
          <div className="form-group">
            <label className="control-label" htmlFor="stateSelect">
              State
            </label>
            <select
              className="form-control"
              id="stateSelect"
              onChange={(event) => updateState(event.target.value)}
              value={state}
            >
              <option key="choose_state" value="">
                {chooseStatePlaceholder()}
              </option>
              {statesResponse.data?.map(({ short, name }) => (
                <option key={short} value={short}>
                  {name}
                </option>
              ))}
            </select>
          </div>

          <div className="form-group">
            <label className="control-label" htmlFor="citySelect">
              City
            </label>
            <select
              className="form-control"
              id="citySelect"
              onChange={(event) => updateCity(event.target.value)}
              value={city}
            >
              <option key="choose_city" value="">
                {chooseCityPlaceholder()}
              </option>
              {state &&
                citiesResponse.data?.map(({ name }) => (
                  <option key={name} value={name}>
                    {name}
                  </option>
                ))}
            </select>
          </div>
        </form>

        {city && restaurantsResponse.error && (
          <p aria-live="polite" className="restaurant">
            Error loading restaurants: {restaurantsResponse.error.message}
          </p>
        )}

        {city && restaurantsResponse.isPending && (
          <p aria-live="polite" className="restaurant loading">
            Loading restaurants…
          </p>
        )}

        {city &&
          restaurantsResponse.data &&
          (restaurantsResponse.data.length === 0
            ? !restaurantsResponse.isPending && (
                <p aria-live="polite">No restaurants found.</p>
              )
            : restaurantsResponse.data.map(
                ({ _id, slug, name, address, images }) => (
                  <ListItem
                    key={_id}
                    address={address}
                    name={name}
                    slug={slug}
                    thumbnail={images.thumbnail}
                  />
                ),
              ))}
      </div>
    </>
  )
}

export default RestaurantList

```

✏️ Update **src/services/pmo/restaurant/hooks.ts** to be:

```
import type { City, Restaurant, State } from "./interfaces"
import { useEffect, useState } from "react"
import { apiRequest } from "../api"

interface CitiesResponse {
  data: City[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface RestaurantsResponse {
  data: Restaurant[] | undefined
  error: Error | undefined
  isPending: boolean
}

interface StatesResponse {
  data: State[] | undefined
  error: Error | undefined
  isPending: boolean
}

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<City[]>({
        method: "GET",
        path: "/cities",
        params: {
          state: state,
        },
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [state])

  return response
}

export function useRestaurants(
  state: string,
  city: string,
): RestaurantsResponse {
  const [response, setResponse] = useState<RestaurantsResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<Restaurant[]>({
        method: "GET",
        path: "/restaurants",
        params: {
          "filter[address.state]": state,
          "filter[address.city]": city,
        },
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [state, city])

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<State[]>({
        method: "GET",
        path: "/states",
      })

      setResponse({
        data: data,
        error: error,
        isPending: false,
      })
    }
    fetchData()
  }, [])

  return response
}

```

**Having issues with your local setup?** See the solution in [StackBlitz](https://stackblitz.com/fork/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/04-solution?file=src/services/pmo/restaurant/hooks.ts) or [CodeSandbox](https://codesandbox.io/p/devbox/github/bitovi/academy/tree/main/exercises/react-vite/09-making-http-requests/04-solution?file=src/services/pmo/restaurant/hooks.ts).

## Next steps

Next, let’s learn how to have [child routes within parent routes](https://www.bitovi.com/academy/learn-react/nested-routes.html?hsLang=en-us) with React Router.

###### 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/making-http-requests.html",
          "description": "Learn about how to make `fetch` requests and render requested data in React components. 
"
     },
     "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"
     ]
}
```