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

[Bitovi](https://www.bitovi.com/?hsLang=en-us)

- [Services](https://www.bitovi.com/services?hsLang=en-us)
- [Client Work](https://www.bitovi.com/client-work?hsLang=en-us)
- [Our Team](https://www.bitovi.com/about?hsLang=en-us)
- [Open Source](https://www.bitovi.com/open-source?hsLang=en-us)
- [Blog](https://www.bitovi.com/blog?hsLang=en-us)
- [Academy](https://www.bitovi.com/academy/?hsLang=en-us)

[Contact Us](https://www.bitovi.com/contact?hsLang=en-us)

- [ 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 ](https://www.bitovi.com/academy/learn-react-vite.html?hsLang=en-us) 
    - [ Introduction to React ](https://www.bitovi.com/academy/learn-react-vite/intro-to-react.html?hsLang=en-us)
    - [ Setting Up Your Environment ](https://www.bitovi.com/academy/learn-react-vite/setting-up-your-environment.html?hsLang=en-us)
    - [ Introduction to JSX ](https://www.bitovi.com/academy/learn-react-vite/intro-to-jsx.html?hsLang=en-us)
    - [ Building Components ](https://www.bitovi.com/academy/learn-react-vite/components.html?hsLang=en-us)
    - [ Passing Props ](https://www.bitovi.com/academy/learn-react-vite/props.html?hsLang=en-us)
    - [ Routing in React ](https://www.bitovi.com/academy/learn-react-vite/routing.html?hsLang=en-us)
    - [ Styling in React ](https://www.bitovi.com/academy/learn-react-vite/styling-in-react.html?hsLang=en-us)
    - [ Managing State in React ](https://www.bitovi.com/academy/learn-react-vite/stateful-hooks.html?hsLang=en-us)
    - [ Making HTTP Requests ](https://www.bitovi.com/academy/learn-react-vite/making-http-requests.html?hsLang=en-us)
    - [ Creating Nested Routes ](https://www.bitovi.com/academy/learn-react-vite/nested-routes.html?hsLang=en-us)
    - [ Handling User Inputs and Forms ](https://www.bitovi.com/academy/learn-react-vite/controlled-vs-uncontrolled.html?hsLang=en-us)
    - [ Testing in React ](https://www.bitovi.com/academy/learn-react-vite/testing.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)
- [ 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

TODO

## Objective 1: Add a `fetch` request for states

In this section, we will:

- Learn about the `useEffect` Hook
- TODO: Review TypeScript generics?

### The `useEffect` Hook

`useEffect` is a React Hook that lets you perform side effects in your functional 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';

const GeolocationComponent: React.FC = () => {
    const [location, setLocation] = useState(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';

const GeolocationComponent: React.FC = () => {
    const [location, setLocation] = useState(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.

```
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 = 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 = 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.

### TypeScript generics

TODO? We’ve used generics, but maybe explain the ones we’re going to use so it’s a little familiar?

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

TODO: Explain that setting environment variables is a generic thing you do, and on this project in particular, Vite will make anything prefixed with `VITE_` available in our client-side source code.

### Setup

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

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

✏️ 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 { useEffect, useState } from 'react'
import ListItem from './ListItem'
import { useCities } from '../../services/restaurant/hooks'
import { State } from '../../services/restaurant/interfaces'

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

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

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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
```

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

```
{
  "name": "place-my-order",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "api": "place-my-order-api --port 7070",
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "test": "vitest",
    "preview": "vite preview"
  },
  "dependencies": {
    "place-my-order-api": "^1.3.0",
    "place-my-order-assets": "^0.2.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.22.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",
    "@typescript-eslint/eslint-plugin": "^6.14.0",
    "@typescript-eslint/parser": "^6.14.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "jsdom": "^24.0.0",
    "typescript": "^5.2.2",
    "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 second terminal tab to run the `api` command from.

### Verify

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

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

import * as restaurantHooks from '../../services/restaurant/hooks'
import RestaurantList from './RestaurantList';

// Mocking necessary modules
vi.mock('../../services/restaurant/hooks')

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

global.fetch = mockFetch;

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

  mockFetch.mockResolvedValueOnce({
    ok: true,
    json: () => Promise.resolve({ message: 'success' }),
    statusText: 'OK',
    status: 200,
  });
});

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

describe('RestaurantList component', () => {
  beforeEach(async () => {
    vi.spyOn(restaurantHooks, 'useCities').mockReturnValue([
      { name: 'Green Bay' },
      { name: 'Madison' },
    ])
    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)
  })
});

```

### Exercise

- Update `RestaurantList.tsx` to call `useState()` and use the `StateResponse` interface.
- Call `useEffect()` and `fetch` data from `${import.meta.env.VITE_PMO_API}/states`.

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

### Solution

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 { useEffect, useState } from 'react'
import ListItem from './ListItem'
import { useCities } from '../../services/restaurant/hooks'
import { State } from '../../services/restaurant/interfaces'

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

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

  const [statesResponse, setStatesResponse] = useState<StatesResponse>({
    data: null,
    error: null,
    isPending: true,
  })

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

      const data = await response.json()

      setStatesResponse({
        data: data?.data || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, []);

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

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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
```

## Objective 2: Move the fetch to a `useStates` Hook

In this section, we will:

- Refactor our `<RestaurantList>` component to depend on a custom Hook.

### Writing custom Hooks as services

In a previous section, we created a `useCities` Hook in our `hooks.ts` file.

Putting stateful logic into a custom Hook has numerous benefits:

**Reusability:** One of the primary reasons for creating custom Hooks is reusability. You might find yourself repeating the same logic in different components—for example, fetching data from an API, handling form input, or managing a subscription. By refactoring this logic into a custom Hook, you can easily reuse this functionality across multiple components, keeping your code DRY (Don't Repeat Yourself).

**Separation of concerns:** Custom Hooks allow you to separate complex logic from the component logic. This makes your main component code cleaner and more focused on rendering UI, while the custom Hook handles the business logic or side effects. It aligns well with the principle of single responsibility, where a function or module should ideally do one thing only.

**Easier testing and maintenance:** Isolating logic into custom Hooks can make your code easier to test and maintain. Since Hooks are just JavaScript functions, they can be tested independently of any component. This isolation can lead to more robust and reliable code.

**Simplifying components:** If your component is becoming too large and difficult to understand, moving some logic to a custom Hook can simplify it. This not only improves readability but also makes it easier for other developers to grasp what the component is doing.

**Sharing stateful logic:** Custom Hooks can contain stateful logic, which is not possible with regular JavaScript functions. This means you can have a Hook that manages its own state and shares this logic across multiple components, something that would be difficult or impossible with traditional class-based components.

### Setup

✏️ 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 } from 'react'
import ListItem from './ListItem'
import { useCities, useStates } from '../../services/restaurant/hooks'

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

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

  const [statesResponse, setStatesResponse] = useState<StatesResponse>({
    data: null,
    error: null,
    isPending: true,
  })

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

      const data = await response.json()

      setStatesResponse({
        data: data?.data || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, []);

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

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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/restaurant/hooks.ts** to be:

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

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

### Verify

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

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

import * as restaurantHooks from '../../services/restaurant/hooks'
import RestaurantList from './RestaurantList';

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

describe('RestaurantList component', () => {
  beforeEach(async () => {
    vi.spyOn(restaurantHooks, 'useCities').mockReturnValue([
      { name: 'Green Bay' },
      { name: 'Madison' },
    ])
    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/restaurant/hooks.test.ts** to be:

```
import { renderHook, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } 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
        global.fetch = vi.fn();
    })

    it('should set the states data on successful fetch', async () => {
        const mockStates = [{ name: 'State1' }, { name: 'State2' }];
        fetch.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).toBeNull();
        });
    });
});
```

### Exercise

- Refactor the existing `useState` and `useEffect` logic into a new `useStates` Hook.

Hint: After moving the state and effect logic into `hooks.ts`, use your new Hook in `RestaurantList.tsx`.

### Solution

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 } from 'react'
import { useCities, useStates } from '../../services/restaurant/hooks'
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)
  }

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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/restaurant/hooks.ts** to be:

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

interface StatesResponse {
  data: State[] | null;
  error: Error | null;
  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: null,
    error: null,
    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 || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, []);

  return response
}
```

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

In this section, we will:

- Learn about including query parameters in our API calls.

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

✏️ 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 } from 'react'
import { useCities, useStates } from '../../services/restaurant/hooks'
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)
  }

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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="">
                {
                  state
                    ? citiesResponse.isPending
                      ? "Loading cities…"
                      : citiesResponse.error
                        ? citiesResponse.error.message
                        : "Choose a city"
                    : "Choose a state before selecting a city"
                }
              </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

✏️ 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';

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

import { useCities, useStates } from '../../services/restaurant/hooks'

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: null, isPending: true, error: null });
    useCities.mockReturnValue({ data: null, isPending: false, error: null });

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

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

  it('displays error messages correctly', async () => {
    useStates.mockReturnValue({ data: null, isPending: false, error: { message: 'Error loading states' } });
    useCities.mockReturnValue({ data: null, 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: null });
    useCities.mockReturnValue({ data: [{ name: 'Los Angeles' }], isPending: false, error: null });

    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/restaurant/hooks.test.ts** to be:

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

describe('useCities Hook', () => {
    beforeEach(() => {
        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).toBeNull();
            expect(result.current.error).toBeNull();
        });
    });

    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).toBeNull();
        });
    });
});

describe('useStates Hook', () => {
    beforeEach(async () => {
        // Mocking the fetch function
        global.fetch = vi.fn();
    })

    it('should set the states data on successful fetch', async () => {
        const mockStates = [{ name: 'State1' }, { name: 'State2' }];
        fetch.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).toBeNull();
        });
    });
});
```

### Exercise

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

### Solution

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 } from 'react'
import { useCities, useStates } from '../../services/restaurant/hooks'
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)
  }

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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="">
                {
                  state
                    ? citiesResponse.isPending
                      ? "Loading cities…"
                      : citiesResponse.error
                        ? citiesResponse.error.message
                        : "Choose a city"
                    : "Choose a state before selecting a city"
                }
              </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/restaurant/hooks.ts** to be:

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

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

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

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: null,
    error: null,
    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 || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, [state]);

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: null,
    error: null,
    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 || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, []);

  return response
}
```

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

In this section, we will learn how to:

- Handle HTTP error statuses (e.g. `404 Not Found`)
- Catch network errors from `fetch()`

### Checking for error responses

- `.ok`
- `.status`
- `.statusText`

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 ? null : 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`).

### Handling 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 ? null : 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

✏️ Create **src/services/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 | null, error: Error | null }> {
}

export function stringifyQuery(input: Record<string, string>): 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/restaurant/hooks.ts** to be:

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

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

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

export function useCities(state: string): CitiesResponse {
  const [response, setResponse] = useState<CitiesResponse>({
    data: null,
    error: null,
    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 || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, [state]);

  return response
}

export function useStates(): StatesResponse {
  const [response, setResponse] = useState<StatesResponse>({
    data: null,
    error: null,
    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 || null,
        error: null,
        isPending: false,
      })
    }
    fetchData()
  }, []);

  return response
}
```

### Verify

✏️ Create **src/services/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();

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: null });
    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: null, 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/restaurant/hooks.test.ts** to be:

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

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

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' }];
      apiRequest.mockResolvedValue({ data: { data: mockCities }, error: null });

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

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

    it('should handle error when fetching cities data', async () => {
      const mockError = new Error('Error fetching cities');
      apiRequest.mockResolvedValue({ data: null, error: mockError });

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

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy();
        expect(result.current.data).toBeNull();
        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' }];
      apiRequest.mockResolvedValue({ data: { data: mockStates }, error: null });

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

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

    it('should handle error when fetching states data', async () => {
      const mockError = new Error('Error fetching states');
      apiRequest.mockResolvedValue({ data: null, error: mockError });

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

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

```

### Exercise

- 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",
})
```

### Solution

Click to see the solution

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

```
export async function apiRequest<Data = never, Params = unknown>({
    method,
    params,
    path,
}: {
    method: string
    params?: Params
    path: string
}): Promise<{ data: Data | null, error: Error | null }> {
    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 ? null : new Error(`${response.status} (${response.statusText})`)

        return {
            data: data,
            error: error,
        }
    } catch (error) {
        return {
            data: null,
            error: error instanceof Error ? error : new Error('An unknown error occurred'),
        }
    }
}

export function stringifyQuery(input: Record<string, string>): 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/restaurant/hooks.ts** to be:

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

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

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

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

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

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

  return response
}

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

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

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

  return response
}
```

## Objective 5: Fetch restaurant data

In this section, we will:

- Create a `useRestaurants` Hook for fetching the restaurant data.

![](https://bitovi.github.io/academy/static/img/react-vite/09-making-http-requests/5-problem.png)

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

![](https://bitovi.github.io/academy/static/img/react-vite/09-making-http-requests/5-solution.png)

### Setup

✏️ 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 } from 'react'
import { useCities, useRestaurants, useStates } from '../../services/restaurant/hooks'
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)
  }

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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="">
                {
                  state
                    ? citiesResponse.isPending
                      ? "Loading cities…"
                      : citiesResponse.error
                        ? citiesResponse.error.message
                        : "Choose a city"
                    : "Choose a state before selecting a city"
                }
              </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/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/restaurant/hooks.ts** to be:

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

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

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

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

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

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

  return response
}

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

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

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

  return response
}
```

### Verify

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 { describe, expect, it, vi } from 'vitest';

import RestaurantList from './RestaurantList';

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

import { useCities, useRestaurants, useStates } from '../../services/restaurant/hooks'

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', () => {
    useStates.mockReturnValue({ data: null, isPending: true, error: null });
    useCities.mockReturnValue({ data: null, isPending: false, error: null });
    useRestaurants.mockReturnValue({ data: null, isPending: false, error: null });

    render(<RestaurantList />);

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

  it('displays error messages correctly', () => {
    useStates.mockReturnValue({ data: null, isPending: false, error: { message: 'Error loading states' } });
    useCities.mockReturnValue({ data: null, isPending: false, error: { message: 'Error loading cities' } });
    useRestaurants.mockReturnValue({ data: null, isPending: false, error: { message: 'Error loading restaurants' } });

    render(<RestaurantList />);

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

  it('renders restaurants correctly', async () => {
    useStates.mockReturnValue({ data: [{ short: 'CA', name: 'California' }], isPending: false, error: null });
    useCities.mockReturnValue({ data: [{ name: 'Los Angeles' }], isPending: false, error: null });
    useRestaurants.mockReturnValue({ data: [{ _id: '1', slug: 'test-restaurant', name: 'Test Restaurant', address: '123 Test St', images: { thumbnail: 'test.jpg' } }], isPending: false, error: null });

    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/restaurant/hooks.test.ts** to be:

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

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

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' }];
      apiRequest.mockResolvedValue({ data: { data: mockCities }, error: null });

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

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

    it('should handle error when fetching cities data', async () => {
      const mockError = new Error('Error fetching cities');
      apiRequest.mockResolvedValue({ data: null, error: mockError });

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

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy();
        expect(result.current.data).toBeNull();
        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' }];
      apiRequest.mockResolvedValue({ data: { data: mockRestaurants }, error: null });

      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).toBeNull();
      });
    });

    it('should handle error when fetching restaurants data', async () => {
      const mockError = new Error('Error fetching restaurants');
      apiRequest.mockResolvedValue({ data: null, error: mockError });

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

      await waitFor(() => {
        expect(result.current.isPending).toBeFalsy();
        expect(result.current.data).toBeNull();
        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' }];
      apiRequest.mockResolvedValue({ data: { data: mockStates }, error: null });

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

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

    it('should handle error when fetching states data', async () => {
      const mockError = new Error('Error fetching states');
      apiRequest.mockResolvedValue({ data: null, error: mockError });

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

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

```

### Exercise

- Implement a `useRestaurants` Hook to fetch restaurant data.
- Update `RestaurantList.tsx` to use your new `useRestaurants` Hook.

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

### Solution

Click to see the solution

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

```
import { useState } from 'react'
import { useCities, useRestaurants, useStates } from '../../services/restaurant/hooks'
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)
  }

  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="">
                {
                  statesResponse.isPending
                    ? "Loading states…"
                    : statesResponse.error
                      ? statesResponse.error.message
                      : "Choose a state"
                }
              </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="">
                {
                  state
                    ? citiesResponse.isPending
                      ? "Loading cities…"
                      : citiesResponse.error
                        ? citiesResponse.error.message
                        : "Choose a city"
                    : "Choose a state before selecting a city"
                }
              </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/restaurant/hooks.ts** to be:

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

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

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

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

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

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

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

  return response
}

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

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

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

  return response
}

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

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

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

  return response
}
```

## Next steps

TODO

###### 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-vite/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"
     ]
}
```