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

- ![AI implementation](https://www.bitovi.com/hubfs/AIConsultingIcon.svg)
  
  [AI implementation](https://www.bitovi.com/services/ai-consulting?hsLang=en-us)
- ![Systems engineering](https://www.bitovi.com/hubfs/icon%20-%20backend.svg)
  
  [Systems engineering](https://www.bitovi.com/services/systems-engineering-consulting?hsLang=en-us)
- ![Project management](https://www.bitovi.com/hubfs/icon%20-%20PM.svg)
  
  [Project management](https://www.bitovi.com/services/agile-project-management-consulting?hsLang=en-us)
- ![Product design](https://www.bitovi.com/hubfs/icon%20-%20design.svg)
  
  [Product design](https://www.bitovi.com/services/product-design-consulting?hsLang=en-us)
- ![Frontend development](https://www.bitovi.com/hubfs/icon%20-%20frontend.svg)
  
  [Frontend development](https://www.bitovi.com/services/frontend-development-consulting?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/digital-consulting-services?hsLang=en-us)

We're Experts in...

- [JavaScript](https://www.bitovi.com/services/frontend/javascript-consulting?hsLang=en-us)
- [AI training](https://www.bitovi.com/ai-training-for-software-engineers?hsLang=en-us)
- [Angular](https://www.bitovi.com/services/frontend/angular-consulting?hsLang=en-us)
- [Design Systems](https://www.bitovi.com/services/axure-figma-migration?hsLang=en-us)
- [React](https://www.bitovi.com/services/frontend/react-consulting?hsLang=en-us)
- [Temporal](https://www.bitovi.com/services/backend/temporal-consulting?hsLang=en-us)
- [React Native](https://www.bitovi.com/services/frontend/react-consulting/react-native?hsLang=en-us)
- [Node.js](https://www.bitovi.com/services/backend/nodejs-consulting-1?hsLang=en-us)

Showcase

![Yum! Brands](https://www.bitovi.com/hubfs/yum-showcase-link-1.png)

[View case study](https://www.bitovi.com/en/bitovi-yum-case-study?hsLang=en-us)

More Projects

- [![Levi's](https://www.bitovi.com/hubfs/levis.svg)](https://www.bitovi.com/web-application-consulting-work/levis-ecommerce-responsive-redesign?hsLang=en-us)
- [![Christie's International Real Estate](https://www.bitovi.com/hubfs/christies.svg)](https://design.bitovi.com/christies)
- [![BAFS](https://www.bitovi.com/hubfs/bafs.svg)](https://www.bitovi.com/ux-design-consulting/ux-case-studies/bafs-ppp?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/our-software-consulting-work?hsLang=en-us)

Open Source Tools

We build powerful tools and open source them to support the community.

[See what we've built →](https://www.bitovi.com/open-source?hsLang=en-us)

- [![Blog](https://www.bitovi.com/hubfs/icon%20-%20blog.svg)
  
  BlogWe post about perfecting software delivery
  
  ](https://bitovi.com/blog)
- [![Partnerships](https://www.bitovi.com/hubfs/Handshake-1.svg)
  
  PartnershipsLearn about Bitovi's technology partners
  
  ](https://www.bitovi.com/partnerships?hsLang=en-us)
- [![Academy](https://www.bitovi.com/hubfs/limbo/icons/icon-academy.svg)
  
  AcademyFree courses to build delivery skills
  
  ](https://www.bitovi.com/academy/?hsLang=en-us)
- [![Open source tools](https://www.bitovi.com/hubfs/icon%20-%20open%20source.svg)
  
  Open source toolsUse our tools or contribute to our open source mission.
  
  ](https://www.bitovi.com/open-source?hsLang=en-us)

Let's Connect

- [![Discord](https://www.bitovi.com/hubfs/DiscordLogo.svg)
  
  Discord
  
  ](https://discord.gg/J7ejFsZnJ4)
- [![LinkedIn](https://www.bitovi.com/hubfs/LinkedinLogo.svg)
  
  LinkedIn
  
  ](https://www.linkedin.com/company/bitovi/)
- [![GitHub](https://www.bitovi.com/hubfs/GithubLogo.svg)
  
  GitHub
  
  ](https://github.com/bitovi/)

![Eggbot](https://www.bitovi.com/hubfs/build_assets/bitovi-limbo-cms-react/338/js_client_assets/assets/eggbot-LTGhdSGL.png)

Name *

Work Email *

Phone

What's your project?

Send

### Contact Us

(312) 620-0386contact@bitovi.com

- [ Advanced JavaScript ](https://www.bitovi.com/academy/learn-advanced-javascript.html?hsLang=en-us)
- [ DOM and jQuery ](https://www.bitovi.com/academy/learn-dom-jquery.html?hsLang=en-us)
- [ Debug JavaScript ](https://www.bitovi.com/academy/learn-to-debug-javascript.html?hsLang=en-us)
- [ Learn TypeScript ](https://www.bitovi.com/academy/learn-typescript.html?hsLang=en-us)
- [ Learn React Native ](https://www.bitovi.com/academy/learn-react-native.html?hsLang=en-us) 
    - [ Introduction to React Native ](https://www.bitovi.com/academy/learn-react-native/intro-to-react-native.html?hsLang=en-us)
    - [ Setting Up Your Environment ](https://www.bitovi.com/academy/learn-react-native/setting-up-your-environment.html?hsLang=en-us)
    - [ Creating a New Application ](https://www.bitovi.com/academy/learn-react-native/creating-a-new-app.html?hsLang=en-us)
    - [ Introduction to JSX ](https://www.bitovi.com/academy/learn-react-native/intro-to-jsx.html?hsLang=en-us)
    - [ Introduction to Testing ](https://www.bitovi.com/academy/learn-react-native/intro-to-testing.html?hsLang=en-us)
    - [ Building Custom Components ](https://www.bitovi.com/academy/learn-react-native/building-custom-components.html?hsLang=en-us)
    - [ Debugging and DevTools ](https://www.bitovi.com/academy/learn-react-native/debugging-devtools.html?hsLang=en-us)
    - [ Styling in React Native ](https://www.bitovi.com/academy/learn-react-native/styling.html?hsLang=en-us)
    - [ Using React Context ](https://www.bitovi.com/academy/learn-react-native/react-context.html?hsLang=en-us)
    - [ Managing State with useState ](https://www.bitovi.com/academy/learn-react-native/managing-state.html?hsLang=en-us)
    - [ Navigation in React Native ](https://www.bitovi.com/academy/learn-react-native/navigation.html?hsLang=en-us)
    - [ Storing State in Navigation Parameters ](https://www.bitovi.com/academy/learn-react-native/navigation-params.html?hsLang=en-us)
    - [ Making HTTP Requests ](https://www.bitovi.com/academy/learn-react-native/making-http-requests.html?hsLang=en-us)
    - [ Handling User Inputs ](https://www.bitovi.com/academy/learn-react-native/user-inputs.html?hsLang=en-us)
    - [ Using Async Storage ](https://www.bitovi.com/academy/learn-react-native/using-asyncstorage.html?hsLang=en-us)
    - [ Security and Authentication ](https://www.bitovi.com/academy/learn-react-native/security-and-auth.html?hsLang=en-us)
    - [ Adding Offline Support ](https://www.bitovi.com/academy/learn-react-native/offline-support.html?hsLang=en-us)
    - [ Integrating Maps ](https://www.bitovi.com/academy/learn-react-native/google-maps.html?hsLang=en-us)
    - [ Performance and Optimization ](https://www.bitovi.com/academy/learn-react-native/performance-optimization.html?hsLang=en-us)
    - [ Building React Native Apps ](https://www.bitovi.com/academy/learn-react-native/building.html?hsLang=en-us)
    - [ Publishing and Updating ](https://www.bitovi.com/academy/learn-react-native/publishing-and-updating.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 Native components.

## Overview

In this section, you will:

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

## Objective 1: Fetch states in a custom Hook

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

![Screenshot of the application when it makes an API call to the states endpoint and is populated the list of states.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/1-solution.png)

### Defining interfaces for `useState`

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

```
import { useState } from "react"
import { View, Text, Pressable } from "react-native"

interface UserProfile {
  email: string
  name: string
}

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

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

  return (
    <View>
      <Text>Name: {userProfile.name}</Text>
      <Text>Email: {userProfile.email}</Text>
      <Pressable onPress={updateProfile}>
        <Text>Update profile</Text>
      </Pressable>
    </View>
  )
}

export default UserProfileComponent

```

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

### The `useEffect` Hook

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

Here is an example component with `useEffect`:

```
import { useEffect, useState } from "react"
import { Keyboard, Text, View } from "react-native"

const GeolocationComponent: React.FC = () => {
  const [keyboardStatus, setKeyboardStatus] = useState("")

  useEffect(() => {
    // Effect callback function
    const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
      setKeyboardStatus("Keyboard shown")
    })
    const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
      setKeyboardStatus("Keyboard hidden")
    })

    return () => {
      // Teardown function
      showSubscription.remove()
      hideSubscription.remove()
    }
  }, []) // Dependency array

  return (
    <View>
      {keyboardStatus ? (
        <Text>{keyboardStatus}</Text>
      ) : (
        <Text>Checking keyboard status…</Text>
      )}
    </View>
  )
}

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 UI in React Native components.

The key aspect of this function is that it’s executed after the component renders. The effects in `useEffect` don’t block the UI 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"
import { Keyboard, Text, View } from "react-native"

const GeolocationComponent: React.FC = () => {
  const [keyboardStatus, setKeyboardStatus] = useState("")

  useEffect(() => {
    // Effect callback function
    const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
      setKeyboardStatus("Keyboard shown")
    })
    const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
      setKeyboardStatus("Keyboard hidden")
    })

    return () => {
      // Teardown function
      showSubscription.remove()
      hideSubscription.remove()
    }
  }, []) // Dependency array

  return (
    <View>
      {keyboardStatus ? (
        <Text>{keyboardStatus}</Text>
      ) : (
        <Text>Checking keyboard status…</Text>
      )}
    </View>
  )
}

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"
import { Pressable, Vibration, View } from "react-native"

function VibrateButtons() {
  const [vibrate, setVibrate] = useState(false)

  useEffect(() => {
    if (vibrate) {
      Vibration.vibrate(1000)
    }

    return () => {
      Vibration.cancel()
    }
  }, [vibrate])

  return (
    <View>
      <Pressable onPress={() => setVibrate(true)}>
        <Text>Vibrate</Text>
      </Pressable>
      <Pressable onPress={() => setVibrate(false)}>
        <Text>Stop vibrating</Text>
      </Pressable>
    </View>
  )
}

export default VibrateButtons

```

#### No dependency array

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

```
import { useEffect, useState } from "react"
import { Pressable, Text } from "react-native"

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

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

  return (
    <Pressable onPress={() => setCount(count + 1)}>
      <Text>Increment</Text>
    </Pressable>
  )
}

export default UpdateLogger

```

### Async operations inside `useEffect`

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

```
import { useEffect, useState } from "react"
import { Text } from "react-native"

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

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

  return <Text>{data}</Text>
}

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"
import { Text } from "react-native"

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

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

    fetchData()
  }, [])

  return <Text>{data}</Text>
}

export default DataFetcher

```

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

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

### Cleanup functions

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

```
import { useEffect, useState } from "react"
import { View, FlatList, Text } from "react-native"

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 (
    <View>
      <FlatList
        data={messages}
        renderItem={({ item }) => <Text>{item}</Text>}
        keyExtractor={(item, index) => index.toString()}
      />
    </View>
  )
}

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 UI, the cleanup function will run and tear down the WebSocket connection.

### Environment variables

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

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

This is particularly useful for:

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

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

#### Using environment variables with react-native-dotenv

When using React Native with environment variables, you can utilize a package like "react-native-dotenv" to manage environment variables. Here’s how you can adapt the provided context for a React Native application:

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

```
PMO_API=http://localhost:7070

```

Then we can access this variable using `process.env.PMO_API`:

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

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

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

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

### Setup 1

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

✏️ Install the new dev dependency:

```
npm install --save-dev place-my-order-api@1 react-native-dotenv@3
```

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

```
PMO_API=http://localhost:7070

```

✏️ Duplicate **.env.example** to **.env** in your project.

It’s always a good idea to keep a `.env.example` file up to date (and committed to git) in your project, then include the actual secrets in your local `.env` file (and not committed to git).

We do not have any secret values yet, so our `.env` can be an exact duplicate of the `.env.example` file.

✏️ Update **.gitignore** to be:

```
# OSX
#
.DS_Store

# Windows
#
Thumbs.db

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
**/.xcode.env.local

# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore

# node.js
#
node_modules/
npm-debug.log
yarn-error.log

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/

**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output

# Bundle artifact
*.jsbundle
*.tsbuildinfo

# Environment variables
.env

# Place My Order API
/db-data

# Ruby / CocoaPods
**/Pods/
/vendor/bundle/

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*

# testing
/coverage

# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

```

✏️ Update **babel.config.js** to be:

```
module.exports = {
  presets: ["module:@react-native/babel-preset"],
  plugins: [
    "module:react-native-dotenv",
    [
      "module-resolver",
      {
        root: ["./"],
        extensions: [".js", ".jsx", ".tsx", ".ts", ".ios.js", ".android.js"],
        alias: {
          "@shared": "./src/shared",
        },
      },
    ],
  ],
}

```

✏️ Create **src/env.d.ts** and update it to be:

```
/* eslint-disable @typescript-eslint/no-namespace */

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly PMO_API: string
    }
  }
}

export {}

```

✏️ Terminate the existing dev server and start it again:

```
npm run start
```

✏️ Update **package.json** to be:

```
{
  "name": "PlaceMyOrder",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "android:linkLocalhost": "adb reverse tcp:7070 tcp:7070",
    "api": "npm run android:linkLocalhost && place-my-order-api --port 7070",
    "clean": "rm -rf tsconfig.tsbuildinfo coverage android/.gradle android/build android/app/build node_modules/.cache",
    "depcheck": "depcheck .",
    "devtools": "react-devtools",
    "eslint": "eslint .",
    "eslint:fix": "eslint --fix .",
    "ios": "react-native run-ios",
    "lint": "npm run eslint && npm run prettier",
    "lint:fix": "npm run eslint:fix && npm run prettier:fix",
    "precheck": "npm run typecheck && npm run lint && npm run depcheck && npm test",
    "prettier": "prettier --check .",
    "prettier:fix": "prettier --write .",
    "start": "react-native start --reset-cache --experimental-debugger",
    "test": "jest",
    "test:inspect": "node --inspect-brk ./node_modules/.bin/jest --watch",
    "test:watch": "jest --watch",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@react-navigation/bottom-tabs": "^6.5.20",
    "@react-navigation/native": "^6.1.17",
    "@react-navigation/stack": "^6.3.29",
    "react": "18.2.0",
    "react-native": "0.74.1",
    "react-native-gesture-handler": "^2.16.2",
    "react-native-safe-area-context": "^4.10.1",
    "react-native-screens": "^3.31.1",
    "react-native-vector-icons": "^10.1.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "@babel/runtime": "^7.20.0",
    "@bitovi/eslint-config": "^1.8.0",
    "@react-native/babel-preset": "0.74.83",
    "@react-native/eslint-config": "0.74.83",
    "@react-native/metro-config": "0.74.83",
    "@react-native/typescript-config": "0.74.83",
    "@testing-library/react-native": "^12.5.0",
    "@types/jest": "^29.5.12",
    "@types/react": "^18.2.6",
    "@types/react-native-vector-icons": "^6.4.18",
    "@types/react-test-renderer": "^18.0.0",
    "babel-jest": "^29.6.3",
    "babel-plugin-module-resolver": "^5.0.2",
    "depcheck": "^1.4.7",
    "eslint": "^8.19.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^29.6.3",
    "place-my-order-api": "^1.7.0",
    "prettier": "2.8.8",
    "react-devtools": "^5.2.0",
    "react-native-dotenv": "^3.4.11",
    "react-test-renderer": "18.2.0",
    "typescript": "5.0.4"
  },
  "engines": {
    "node": ">=20"
  }
}

```

✏️ In a **new** command-line interface (CLI) window, start the API server by running:

```
npm run api
```

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

It will be helpful to have a third command-line interface (CLI) tab for the `npm run api` command.

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

```
import { render, screen } from "@testing-library/react-native"

import App from "./App"

const oldFetch = global.fetch
const mockFetch = jest.fn()
beforeAll(() => {
  global.fetch = mockFetch
})
afterAll(() => {
  global.fetch = oldFetch
})

describe("App", () => {
  const mockStateResponse = {
    data: [
      { short: "MI", name: "Michigan" },
      { short: "WI", name: "Wisconsin" },
      { short: "IL", name: "Illinois" },
    ],
  }

  it("renders", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockStateResponse),
      statusText: "OK",
      status: 200,
    })

    render(<App />)

    const placeMyOrderText = await screen.findAllByText(/Place my order/i)
    expect(placeMyOrderText).toHaveLength(2)
  })
})

```

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

```
export interface State {
  name: string
  short: string
}

```

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

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

import { State } from "./interfaces"

const baseUrl = process.env.PMO_API

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

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

```

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

```
export * from "./interfaces"
export * from "./hooks"

```

✏️ Create **src/shared/components/Loading/Loading.tsx** and update it to be:

```
import Box from "@shared/design/Box"
import { useTheme } from "@shared/design/theme"
import Typography from "@shared/design/Typography"
import { ActivityIndicator } from "react-native"

const Loading: React.FC = () => {
  const theme = useTheme()

  return (
    <Box padding="l">
      <ActivityIndicator size="large" color={theme.palette.primary.main} />
      <Typography variant="body" style={{ textAlign: "center", marginTop: 8 }}>
        Loading…
      </Typography>
    </Box>
  )
}

export default Loading

```

✏️ Create **src/shared/components/Loading/Loading.test.tsx** and update it to be:

```
import { render, screen } from "@testing-library/react-native"

import Loading from "./Loading"

describe("Components/Loading", () => {
  it("renders", () => {
    render(<Loading />)
    expect(screen.getByText(/Loading…/)).toBeOnTheScreen()
  })
})

```

✏️ Create **src/shared/components/Loading/index.ts** and update it to be:

```
export { default } from "./Loading"

```

✏️ Update **src/screens/StateList/StateList.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useStates } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

const StateList: React.FC = () => {
  const navigation = useNavigation()
  // Exercise: Update `StateList.tsx` to call `useState()` and use the `StateResponse` interface.

  return (
    <Screen noScroll>
      {states?.length ? (
        <FlatList
          data={states}
          renderItem={({ item: stateItem }) => (
            <Button
              onPress={() => {
                navigation.navigate("CityList", {
                  state: stateItem,
                })
              }}
            >
              {stateItem.name}
            </Button>
          )}
          keyExtractor={(item) => item.short}
        />
      ) : (
        <Typography>No states found</Typography>
      )}
    </Screen>
  )
}

export default StateList

```

### Verify 1

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

```
import { renderHook, waitFor } from "@testing-library/react-native"

import { useStates } from "./hooks"

const oldFetch = global.fetch
const mockFetch = jest.fn()
beforeAll(() => {
  global.fetch = mockFetch
})
afterAll(() => {
  global.fetch = oldFetch
})

describe("Services/PMO/Restaurant/useStates", () => {
  it("returns states", async () => {
    const mockStates = [
      { id: 1, name: "State1" },
      { id: 2, name: "State2" },
    ]
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ message: "success", data: mockStates }),
      statusText: "OK",
      status: 200,
    })

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

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

```

✏️ Update **src/screens/StateList/StateList.test.tsx** to be:

```
import { NavigationContainer } from "@react-navigation/native"
import * as restaurantHooks from "@shared/services/pmo/restaurant/hooks"
import { render, screen } from "@testing-library/react-native"

import StateList from "./StateList"

describe("Screens/StateList", () => {
  // Mock the hooks and components used in StateList
  const mockStateResponse = {
    data: [
      { short: "MI", name: "Michigan" },
      { short: "WI", name: "Wisconsin" },
      { short: "IL", name: "Illinois" },
    ],
  }

  let useStates: jest.SpyInstance<ReturnType<typeof restaurantHooks.useStates>>
  beforeEach(() => {
    jest.resetAllMocks()
    useStates = jest.spyOn(restaurantHooks, "useStates")
  })

  it("renders", () => {
    useStates.mockReturnValue({
      ...mockStateResponse,
      error: undefined,
      isPending: false,
    })

    render(
      <NavigationContainer>
        <StateList />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Michigan/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useStates.mockReturnValue({
      data: undefined,
      error: undefined,
      isPending: true,
    })

    render(
      <NavigationContainer>
        <StateList />
      </NavigationContainer>,
    )

    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })
})

```

### Exercise 1

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

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

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

**Hint:** Use `Array.isArray(data) ? data : data?.data ?? undefined` when you need to check for `data` being an Array or an Object response.

### Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run `npm run test`!

Click to see the solution

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

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

import { State } from "./interfaces"

const baseUrl = process.env.PMO_API

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

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

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${baseUrl}/states`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      })

      const data = await response.json()

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

  return response
}

```

✏️ Update **src/screens/StateList/StateList.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useStates } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

const StateList: React.FC = () => {
  const navigation = useNavigation()
  const { data: states, error, isPending } = useStates()

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Error loading states: </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen noScroll>
      {states?.length ? (
        <FlatList
          data={states}
          renderItem={({ item: stateItem }) => (
            <Button
              onPress={() => {
                navigation.navigate("CityList", {
                  state: stateItem,
                })
              }}
            >
              {stateItem.name}
            </Button>
          )}
          keyExtractor={(item) => item.short}
        />
      ) : (
        <Typography>No states found</Typography>
      )}
    </Screen>
  )
}

export default StateList

```

## Objective 2: Fetch cities in a custom Hook with query parameters

Let’s continue our quest to load data from our API and update our `<CitiesList>` to use a custom `useCities` Hook to fetch data.

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

![Screenshot of the application when it makes an API call to the cities endpoint and is populated the list of cities.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/2-solution.png)

### Including query parameters in API calls

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

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

Here’s a breakdown of this URL:

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

### Setup 2

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

```
export interface State {
  name: string
  short: string
}

export interface City {
  name: string
  state: string
}

```

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

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

import { City, State } from "./interfaces"

const baseUrl = process.env.PMO_API

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

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

interface UseCitiesParams {
  state?: string
}

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

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${baseUrl}/states`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      })

      const data = await response.json()

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

  return response
}

export function useCities({ state }: UseCitiesParams): CitiesResponse {
  // Exercise: Update our `useCities()` Hook to fetch cities from the Place My Order API, given a selected state.
}

```

✏️ Update **src/screens/CityList/CityList.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useCities } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"

export interface CityListProps
  extends StackScreenProps<RestaurantsStackParamList, "CityList"> {}

const CityList: React.FC<CityListProps> = ({ route }) => {
  const { state } = route.params
  const navigation = useNavigation()
  // Exercise: When calling the Place My Order API, include the `state` query parameter.

  return (
    <Screen noScroll>
      <FlatList
        data={cities}
        renderItem={({ item: cityItem }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantList", {
                state,
                city: cityItem,
              })
            }}
          >
            {cityItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.name}
      />
    </Screen>
  )
}

export default CityList

```

### Verify 2

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

```
import { renderHook, waitFor } from "@testing-library/react-native"

import { useStates, useCities } from "./hooks"

const oldFetch = global.fetch
const mockFetch = jest.fn()
beforeAll(() => {
  global.fetch = mockFetch
})
afterAll(() => {
  global.fetch = oldFetch
})

describe("Services/PMO/Restaurant/useStates", () => {
  it("returns states", async () => {
    const mockStates = [
      { id: 1, name: "State1" },
      { id: 2, name: "State2" },
    ]
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ message: "success", data: mockStates }),
      statusText: "OK",
      status: 200,
    })

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

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

describe("Services/PMO/Restaurant/useCities", () => {
  it("returns cities", async () => {
    const mockCities = [
      { id: 1, name: "City1" },
      { id: 2, name: "City2" },
    ]
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve({ message: "success", data: mockCities }),
      statusText: "OK",
      status: 200,
    })

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

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

```

✏️ Update **src/screens/CityList/CityList.test.tsx** to be:

```
import { NavigationContainer } from "@react-navigation/native"
import * as restaurantHooks from "@shared/services/pmo/restaurant/hooks"
import { render, screen } from "@testing-library/react-native"

import CityList from "./CityList"

const route = {
  key: "RestaurantDetails",
  name: "RestaurantDetails",
  params: {
    state: {
      name: "name",
      short: "short",
    },
  },
} as const

describe("Screens/CityList", () => {
  // Mock the hooks and components used in CityList

  const mockCitiesResponse = {
    data: [
      { name: "Detroit", state: "MI" },
      { name: "Ann Arbor", state: "MI" },
    ],
  }
  let useCities: jest.SpyInstance<ReturnType<typeof restaurantHooks.useCities>>
  beforeEach(() => {
    jest.resetAllMocks()
    useCities = jest.spyOn(restaurantHooks, "useCities")
  })

  it("renders", () => {
    useCities.mockReturnValue({
      ...mockCitiesResponse,
      error: undefined,
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <CityList route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Detroit/i)).toBeOnTheScreen()
    expect(screen.getByText(/Ann Arbor/i)).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useCities.mockReturnValue({
      data: undefined,
      error: undefined,
      isPending: true,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <CityList route={route} />
      </NavigationContainer>,
    )

    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })
})

```

### Exercise 2

- Update our `useCities()` Hook to fetch cities from the Place My Order API, given a selected state.
- When calling the Place My Order API, include the `state` query parameter: `http://localhost:7070/cities?state=MO`

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

### Solution 2

If you’ve implemented the solution correctly, the tests will pass when you run `npm run test`!

Click to see the solution

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

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

import { City, State } from "./interfaces"

const baseUrl = process.env.PMO_API

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

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

interface UseCitiesParams {
  state?: string
}

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

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${baseUrl}/states`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      })

      const data = await response.json()

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

  return response
}

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

  useEffect(() => {
    const fetchData = async () => {
      if (state) {
        const response = await fetch(`${baseUrl}/cities?state=${state}`, {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
          },
        })

        const data = await response.json()

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

  return response
}

```

✏️ Update **src/screens/CityList/CityList.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useCities } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"

export interface CityListProps
  extends StackScreenProps<RestaurantsStackParamList, "CityList"> {}

const CityList: React.FC<CityListProps> = ({ route }) => {
  const { state } = route.params
  const navigation = useNavigation()
  const {
    data: cities,
    error,
    isPending,
  } = useCities({ state: state.short || "" })

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Error loading cities: </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen noScroll>
      <FlatList
        data={cities}
        renderItem={({ item: cityItem }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantList", {
                state,
                city: cityItem,
              })
            }}
          >
            {cityItem.name}
          </Button>
        )}
        keyExtractor={(item) => item.name}
      />
    </Screen>
  )
}

export default CityList

```

## Objective 3: Create API request helper

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

While we do this, let‘s add error handling for unsuccessful API requests:

![Screenshot of the application when it makes an API call to the cities endpoint and is populated the list of cities.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/2-solution.png) ![Screenshot of the application when it makes an API call and the API returns an error.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/3-solution.png)

### Handle HTTP error statuses

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

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

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

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

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

```

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

### Catch network errors

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

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

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

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

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

```

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

**Note:** This implementation demonstrates a basic approach to data fetching using fetch. While it works for simple use cases, it lacks important features that production-ready apps typically need — such as caching, refetching, error retries, and background updates.

As your app grows, maintaining these features manually becomes complex and error-prone. At that point, we strongly recommend using a dedicated data-fetching library like [TanStack Query](https://tanstack.com/query/latest) to handle these concerns efficiently.

### Setup 3

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

```
const baseUrl = process.env.PMO_API

export async function apiRequest<
  Data = never,
  Params = unknown,
  Body = unknown,
>({
  method,
  params,
  path,
  body,
}: {
  method: string
  params?: Params
  path: string
  body?: Body
}): Promise<{ data?: Data; error?: Error }> {}

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

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

  // Exercise: Implement the `apiRequest` helper function to handle errors returned and thrown from `fetch()`.

  return output.join("&")
}

```

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

```
export * from "./api"

```

✏️ Update **jest-setup.ts** to be:

```
import "@testing-library/react-native/extend-expect"

import "react-native-gesture-handler/jestSetup"

jest.mock("@react-navigation/native", () => {
  const actualNav = jest.requireActual("@react-navigation/native")
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      setOptions: jest.fn(),
    }),
  }
})

jest.mock("@shared/services/pmo/api", () =>
  require("@shared/services/pmo/api/api.mock"),
)

const consoleError = console.error
console.error = (message, ...args) => {
  if (
    typeof message === "string" &&
    message.match(
      /Warning: An update to .+ inside a test was not wrapped in act\(\.\.\.\)\./,
    )
  ) {
    return
  }

  return consoleError(message, ...args)
}

```

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

```
import { apiRequest } from "@shared/services/pmo/api"
import { useEffect, useState } from "react"

import { City, State } from "./interfaces"

const baseUrl = process.env.PMO_API

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

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

interface UseCitiesParams {
  state?: string
}

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

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${baseUrl}/states`, {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      })

      const data = await response.json()

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

  return response
}

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

  useEffect(() => {
    const fetchData = async () => {
      if (state) {
        const response = await fetch(`${baseUrl}/cities?state=${state}`, {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
          },
        })

        const data = await response.json()

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

  return response
}

```

### Verify 3

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

```
export const apiRequest = async <
  Data = never,
  Params = unknown,
  Body = unknown,
>({
  method,
  params,
  path,
  body,
}: {
  method: string
  params?: Params
  path: string
  body?: Body
}): Promise<{ data?: Data; error?: Error }> => {
  return {}
}

```

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

```
import { apiRequest, stringifyQuery } from "./api"

const oldFetch = global.fetch
const mockFetch = jest.fn()
beforeAll(() => {
  global.fetch = mockFetch
})
afterAll(() => {
  global.fetch = oldFetch
})

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

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

    expect(response).toEqual({ data: { message: "success" }, error: undefined })
    expect(mockFetch).toHaveBeenCalledWith(`${process.env.PMO_API}/test?`, {
      method: "GET",
      body: undefined,
      headers: {
        "Content-Type": "application/json",
      },
    })
  })

  it("handles 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("handles network errors", async () => {
    // Mock a network error
    mockFetch.mockRejectedValueOnce(new Error("Network Error"))

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

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

describe("Services/PMO/API/stringifyQuery", () => {
  it("stringifies query parameters", () => {
    const query = stringifyQuery({ foo: "bar", baz: "qux" })
    expect(query).toBe("foo=bar&baz=qux")
  })

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

```

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

```
import * as api from "@shared/services/pmo/api"
import { renderHook, waitFor } from "@testing-library/react-native"

import { useStates, useCities } from "./hooks"

let apiRequest: jest.SpyInstance<ReturnType<typeof api.apiRequest>>
beforeEach(() => {
  jest.resetAllMocks()
  apiRequest = jest.spyOn(api, "apiRequest")
})

describe("Services/PMO/Restaurant/useStates", () => {
  it("returns states", async () => {
    const mockStates = [
      { id: 1, name: "State1" },
      { id: 2, name: "State2" },
    ]
    apiRequest.mockResolvedValue({
      data: mockStates,
      error: undefined,
    })

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

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching states")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

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

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

describe("Services/PMO/Restaurant/useCities", () => {
  it("returns cities", async () => {
    const mockCities = [
      { id: 1, name: "City1" },
      { id: 2, name: "City2" },
    ]
    apiRequest.mockResolvedValue({
      data: mockCities,
      error: undefined,
    })

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

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching cities")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

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

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

```

### Exercise 3

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

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

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

### Solution 3

If you’ve implemented the solution correctly, the tests will pass when you run `npm run test`!

Click to see the solution

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

```
const baseUrl = process.env.PMO_API

export async function apiRequest<
  Data = never,
  Params = unknown,
  Body = unknown,
>({
  method,
  params,
  path,
  body,
}: {
  method: string
  params?: Params
  path: string
  body?: Body
}): Promise<{ data?: Data; error?: Error }> {
  try {
    const query = params ? stringifyQuery(params) : ""
    const requestUrl = `${baseUrl}${path}?${query}`

    const response = await fetch(requestUrl, {
      method,
      headers: {
        "Content-Type": "application/json",
      },
      body: body ? JSON.stringify(body) : undefined,
    })

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

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

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

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

  return output.join("&")
}

```

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

```
import { apiRequest } from "@shared/services/pmo/api"
import { useEffect, useState } from "react"

import { City, State } from "./interfaces"

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

interface UseCitiesParams {
  state?: string
}

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

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

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

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

  return response
}

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

  useEffect(() => {
    const fetchData = async () => {
      setResponse({
        data: undefined,
        error: undefined,
        isPending: true,
      })

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

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

  return response
}

```

## Objective 4: Fetch restaurants in a custom Hook

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

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

![Screenshot of the application when it makes an API call to the restaurants endpoint and is populated the list of restaurants.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/4-1-solution.png) ![Screenshot of the application when it makes an API call to the restaurant endpoint and is populated populated with the restaurants details.](https://bitovi.github.io/academy/static/img/react-native/13-making-http-requests/4-2-solution.png)

### Setup 4

✏️ Update **.env** to be:

```
PMO_API=http://localhost:7070
PMO_ASSETS=https://www.place-my-order.com/

```

✏️ Update **.env.example** to be:

```
PMO_API=http://localhost:7070
PMO_ASSETS=https://www.place-my-order.com/

```

✏️ Update **src/env.d.ts** to be:

```
/* eslint-disable @typescript-eslint/no-namespace */

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      readonly PMO_API: string
      readonly PMO_ASSETS: string
    }
  }
}

export {}

```

✏️ Terminate the existing dev server and start it again:

```
npm run start
```

✏️ Update **src/shared/components/RestaurantHeader/RestaurantHeader.tsx** to be:

```
import Box from "@shared/design/Box"
import { Theme, useTheme } from "@shared/design/theme"
import Typography from "@shared/design/Typography"
import { ImageBackground, StyleSheet } from "react-native"

const assetsUrl = process.env.PMO_ASSETS

export interface RestaurantHeaderProps {
  restaurant?: {
    _id: string
    address?: {
      city: string
      state: string
      street: string
      zip: string
    }
    images: {
      banner: string
    }
    name: string
    slug: string
  }
}

const RestaurantHeader: React.FC<RestaurantHeaderProps> = ({ restaurant }) => {
  const theme = useTheme()
  const styles = getStyles(theme)

  return (
    <Box>
      <ImageBackground
        style={styles.heroBackground}
        source={{ uri: `${assetsUrl}/${restaurant?.images.banner}` }}
      >
        <Box padding={["xs", "m"]} margin={["s", "none"]} style={styles.hero}>
          <Typography variant="heading" style={styles.heroText}>
            {restaurant?.name}
          </Typography>
        </Box>
      </ImageBackground>

      <Box padding="m">
        {restaurant?.address && (
          <Typography variant="body">
            {restaurant.address.street}
            {restaurant.address.city}, {restaurant.address.state}{" "}
            {restaurant.address.zip}
          </Typography>
        )}

        <Typography variant="body">
          $$$ Hours: M-F 10am-11pm Open Now
        </Typography>
      </Box>
    </Box>
  )
}

function getStyles(theme: Theme) {
  return StyleSheet.create({
    heroBackground: {
      width: "100%",
      maxWidth: 768,
      height: 180,
      margin: "auto",
      justifyContent: "flex-end",
      alignItems: "flex-start",
    },
    hero: {
      backgroundColor: theme.palette.secondary.main + "bb",
    },
    heroText: {
      color: theme.palette.secondary.contrast,
      fontSize: 32,
    },
  })
}

export default RestaurantHeader

```

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

```
export interface State {
  name: string
  short: string
}

export interface City {
  name: string
  state: string
}

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

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

interface Coordinate {
  latitude: number
  longitude: number
}

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

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

interface MenuItem {
  name: string
  price: number
}

```

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

```
import { apiRequest } from "@shared/services/pmo/api"
import { useEffect, useState } from "react"

import { City, Restaurant, State } from "./interfaces"

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

interface UseCitiesParams {
  state?: string
}
interface RestaurantResponse {
  data: Restaurant | undefined
  error: Error | undefined
  isPending: boolean
}

interface UseRestaurant {
  slug: string
}

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

interface UseRestaurants {
  state: string
  city: string
}

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

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

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

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

  return response
}

export function useRestaurant({ slug }: UseRestaurant): RestaurantResponse {
  // Exercise: Fill in `useRestaurant` Hook for fetching the details of the restaurant.
}

export function useRestaurants({
  state,
  city,
}: UseRestaurants): RestaurantsResponse {
  // Exercise: Fill in `useRestaurants` Hook for fetching the list of restaurants.
}

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

  useEffect(() => {
    const fetchData = async () => {
      setResponse({
        data: undefined,
        error: undefined,
        isPending: true,
      })

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

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

  return response
}

```

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

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useRestaurants } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"

export interface RestaurantListProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}

const RestaurantList: React.FC<RestaurantListProps> = ({ route }) => {
  const navigation = useNavigation()
  // Exercise: Update `RestaurantList.tsx` to use our new `useRestaurants` Hook.

  return (
    <Screen noScroll>
      <FlatList
        data={restaurants}
        renderItem={({ item: restaurant }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantDetails", {
                slug: restaurant.slug,
              })
            }}
          >
            {restaurant.name}
          </Button>
        )}
        keyExtractor={(item) => item._id}
      />
    </Screen>
  )
}

export default RestaurantList

```

✏️ Update **src/screens/RestaurantDetails/RestaurantDetails.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import RestaurantHeader from "@shared/components/RestaurantHeader"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useRestaurant } from "@shared/services/pmo/restaurant"
import { useEffect } from "react"

import { RestaurantsStackParamList } from "../../App"

export interface RestaurantDetailsProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantDetails"> {}

const RestaurantDetails: React.FC<RestaurantDetailsProps> = ({ route }) => {
  const navigation = useNavigation()
  // Exercise: Update `RestaurantDetails.tsx` to use our new `useRestaurants` Hook.

  return (
    <Screen>
      <RestaurantHeader restaurant={restaurant} />
      <Button onPress={() => console.warn("Place an order")}>
        Place an order
      </Button>
    </Screen>
  )
}

export default RestaurantDetails

```

### Verify 4

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

```
import * as api from "@shared/services/pmo/api"
import { renderHook, waitFor } from "@testing-library/react-native"

import { useStates, useCities, useRestaurants, useRestaurant } from "./hooks"

// Mock the apiRequest function
let apiRequest: jest.SpyInstance<ReturnType<typeof api.apiRequest>>
beforeEach(() => {
  jest.resetAllMocks()
  apiRequest = jest.spyOn(api, "apiRequest")
})

describe("Services/PMO/Restaurant/useStates", () => {
  it("returns states", async () => {
    const mockStates = [
      { id: 1, name: "State1" },
      { id: 2, name: "State2" },
    ]
    apiRequest.mockResolvedValue({
      data: mockStates,
      error: undefined,
    })

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

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching states")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

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

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

describe("Services/PMO/Restaurant/useCities", () => {
  it("returns cities", async () => {
    const mockCities = [
      { id: 1, name: "City1" },
      { id: 2, name: "City2" },
    ]
    apiRequest.mockResolvedValue({
      data: mockCities,
      error: undefined,
    })

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

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching cities")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

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

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

describe("Services/PMO/Restaurant/useRestaurants", () => {
  it("returns restaurants", async () => {
    const mockRestaurants = [
      { id: 1, name: "Restaurant1" },
      { id: 2, name: "Restaurant2" },
    ]
    apiRequest.mockResolvedValue({
      data: mockRestaurants,
      error: undefined,
    })

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

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching restaurants")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

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

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

describe("Services/PMO/Restaurant/useRestaurant", () => {
  it("returns a restaurant", async () => {
    const mockRestaurant = { id: 1, name: "Restaurant1" }
    apiRequest.mockResolvedValue({
      data: mockRestaurant,
      error: undefined,
    })

    const { result } = renderHook(() => useRestaurant({ slug: "test-slug" }))

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

  it("handles errors", async () => {
    const mockError = new Error("Error fetching restaurant")
    apiRequest.mockResolvedValue({ data: undefined, error: mockError })

    const { result } = renderHook(() => useRestaurant({ slug: "test-slug" }))

    await waitFor(() => {
      expect(result.current.isPending).toBeFalsy()
    })

    expect(result.current.data).toBeUndefined()
    expect(result.current.error).toEqual(mockError)
  })
})

```

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

```
import { NavigationContainer } from "@react-navigation/native"
import * as restaurantHooks from "@shared/services/pmo/restaurant/hooks"
import { render, screen } from "@testing-library/react-native"

import RestaurantList from "./RestaurantList"

const route = {
  key: "RestaurantList",
  name: "RestaurantList",
  params: {
    state: {
      name: "name",
      short: "short",
    },
    city: {
      name: "name",
      state: "state",
    },
    slug: "test",
  },
} as const

describe("Screens/RestaurantList", () => {
  // Mock the hooks and components used in RestaurantList

  const mockRestaurantsResponse = {
    data: [
      {
        name: "Bagel Restaurant",
        slug: "bagel-restaurant",
        images: {
          thumbnail:
            "node_modules/place-my-order-assets/images/3-thumbnail.jpg",
          owner: "node_modules/place-my-order-assets/images/1-owner.jpg",
          banner: "node_modules/place-my-order-assets/images/2-banner.jpg",
        },
        menu: {
          lunch: [
            { name: "Crab Pancakes with Sorrel Syrup", price: 35.99 },
            { name: "Steamed Mussels", price: 21.99 },
            { name: "Roasted Salmon", price: 23.99 },
          ],
          dinner: [
            { name: "Truffle Noodles", price: 14.99 },
            { name: "Spinach Fennel Watercress Ravioli", price: 35.99 },
            { name: "Herring in Lavender Dill Reduction", price: 45.99 },
          ],
        },
        address: {
          street: "285 W Adams Ave",
          city: "Detroit",
          state: "MI",
          zip: "60045",
        },
        coordinate: {
          latitude: 0,
          longitude: 0,
        },
        resources: {
          thumbnail: "api/resources/images/3-thumbnail.jpg",
          owner: "api/resources/images/4-owner.jpg",
          banner: "api/resources/images/1-banner.jpg",
        },
        _id: "5NVE3Z5MXxX3O57R",
      },
      {
        name: "Brunch Barn",
        slug: "brunch-barn",
        images: {
          thumbnail:
            "node_modules/place-my-order-assets/images/4-thumbnail.jpg",
          owner: "node_modules/place-my-order-assets/images/1-owner.jpg",
          banner: "node_modules/place-my-order-assets/images/4-banner.jpg",
        },
        menu: {
          lunch: [
            { name: "Gunthorp Chicken", price: 21.99 },
            { name: "Ricotta Gnocchi", price: 15.99 },
            { name: "Roasted Salmon", price: 23.99 },
          ],
          dinner: [
            { name: "Charred Octopus", price: 25.99 },
            { name: "Herring in Lavender Dill Reduction", price: 45.99 },
            { name: "Steamed Mussels", price: 21.99 },
          ],
        },
        address: {
          street: "285 W Adams Ave",
          city: "Detroit",
          state: "MI",
          zip: "60045",
        },
        coordinate: {
          latitude: 0,
          longitude: 0,
        },
        resources: {
          thumbnail: "api/resources/images/3-thumbnail.jpg",
          owner: "api/resources/images/2-owner.jpg",
          banner: "api/resources/images/4-banner.jpg",
        },
        _id: "AXUIBQBmxrk2EWq8",
      },
    ],
  }

  let useRestaurants: jest.SpyInstance<
    ReturnType<typeof restaurantHooks.useRestaurants>
  >
  beforeEach(() => {
    jest.resetAllMocks()
    useRestaurants = jest.spyOn(restaurantHooks, "useRestaurants")
  })

  it("renders", () => {
    useRestaurants.mockReturnValue({
      ...mockRestaurantsResponse,
      error: undefined,
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantList route={route} />
      </NavigationContainer>,
    )
    expect(screen.getByText(/Bagel Restaurant/i)).toBeOnTheScreen()
    expect(screen.getByText(/Brunch Barn/i)).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useRestaurants.mockReturnValue({
      data: undefined,
      error: undefined,
      isPending: true,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantList route={route} />
      </NavigationContainer>,
    )

    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })

  it("renders error state", () => {
    useRestaurants.mockReturnValue({
      data: undefined,
      error: { name: "Oops", message: "This is the error" },
      isPending: false,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantList route={route} />
      </NavigationContainer>,
    )

    expect(screen.getByText(/Error loading restaurants:/)).toBeOnTheScreen()
    expect(screen.getByText(/This is the error/)).toBeOnTheScreen()
  })
})

```

✏️ Update **src/screens/RestaurantDetails/RestaurantDetails.test.tsx** to be:

```
import { NavigationContainer } from "@react-navigation/native"
import * as restaurantHooks from "@shared/services/pmo/restaurant/hooks"
import { render, screen } from "@testing-library/react-native"

import RestaurantDetails from "./RestaurantDetails"

const route = {
  key: "RestaurantDetails",
  name: "RestaurantDetails",
  params: {
    state: {
      name: "name",
      short: "short",
    },
    city: {
      name: "name",
      state: "state",
    },
    slug: "test",
  },
} as const

jest.mock("@react-navigation/native", () => {
  const actualNav = jest.requireActual("@react-navigation/native")
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      setOptions: jest.fn(),
    }),
  }
})

describe("Screens/RestaurantDetails", () => {
  // Mock the hooks and components used in RestaurantDetails

  let useRestaurant: jest.SpyInstance<
    ReturnType<typeof restaurantHooks.useRestaurant>
  >
  beforeEach(() => {
    jest.resetAllMocks()
    useRestaurant = jest.spyOn(restaurantHooks, "useRestaurant")
  })

  const mockRestaurantData = {
    data: {
      _id: "1",
      name: "Test Restaurant",
      slug: "test-restaurant",
      images: {
        banner: "banner.jpg",
        owner: "owner.jpg",
        thumbnail: "thumbnail.jpg",
      },
      menu: {
        dinner: [{ name: "yum", price: 1 }],
        lunch: [{ name: "snack", price: 2 }],
      },
      coordinate: { latitude: 0, longitude: 0 },
    },
    isPending: false,
    error: undefined,
  }

  it("renders", () => {
    useRestaurant.mockReturnValue(mockRestaurantData)

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantDetails route={route} />
      </NavigationContainer>,
    )

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

  it("renders before data loads", () => {
    useRestaurant.mockReturnValue({ ...mockRestaurantData, data: undefined })
    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantDetails route={route} />
      </NavigationContainer>,
    )

    expect(screen.getByText("Place an order")).toBeOnTheScreen()
  })

  it("renders loading state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      isPending: true,
      error: undefined,
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantDetails route={route} />
      </NavigationContainer>,
    )

    expect(screen.getByText(/Loading/i)).toBeOnTheScreen()
  })

  it("renders error state", () => {
    useRestaurant.mockReturnValue({
      data: undefined,
      isPending: false,
      error: { name: "Error", message: "Mock error" },
    })

    render(
      <NavigationContainer>
        {/* @ts-ignore */}
        <RestaurantDetails route={route} />
      </NavigationContainer>,
    )

    expect(
      screen.getByText(/Error loading restaurant details:/i, {
        exact: false,
      }),
    ).toBeOnTheScreen()
    expect(screen.getByText(/Mock error/i)).toBeOnTheScreen()
  })
})

```

### Exercise 4

- Fill in `useRestaurant` Hook for fetching the details of the restaurant.
- Fill in `useRestaurants` Hook for fetching the list of restaurants.
- Update `RestaurantDetails.tsx` to use our new `useRestaurants` Hook.
- Update `RestaurantList.tsx` to use our new `useRestaurants` Hook.

**Hint:** The `useRestaurant()` Hook should make a request like this: `'/restaurants/${slug}'`

**Hint:** The `useRestaurants()` Hook should make a request with these query parameters: `'/restaurants?filter[address.state]=IL&filter[address.city]=Chicago'`

### Solution 4

If you’ve implemented the solution correctly, the tests will pass when you run `npm run test`!

Click to see the solution

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

```
import { apiRequest } from "@shared/services/pmo/api"
import { useEffect, useState } from "react"

import { City, Restaurant, State } from "./interfaces"

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

interface UseCitiesParams {
  state?: string
}
interface RestaurantResponse {
  data: Restaurant | undefined
  error: Error | undefined
  isPending: boolean
}

interface UseRestaurant {
  slug: string
}

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

interface UseRestaurants {
  state: string
  city: string
}

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

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

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

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

  return response
}

export function useRestaurant({ slug }: UseRestaurant): RestaurantResponse {
  const [response, setResponse] = useState<RestaurantResponse>({
    data: undefined,
    error: undefined,
    isPending: true,
  })

  useEffect(() => {
    const fetchData = async () => {
      const { data, error } = await apiRequest<Restaurant>({
        method: "GET",
        path: `/restaurants/${slug}`,
      })

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

  return response
}

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

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

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

  return response
}

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

  useEffect(() => {
    const fetchData = async () => {
      setResponse({
        data: undefined,
        error: undefined,
        isPending: true,
      })

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

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

  return response
}

```

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

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useRestaurants } from "@shared/services/pmo/restaurant"
import { FlatList } from "react-native"

import { RestaurantsStackParamList } from "../../App"

export interface RestaurantListProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantList"> {}

const RestaurantList: React.FC<RestaurantListProps> = ({ route }) => {
  const navigation = useNavigation()

  const { state, city } = route.params
  const {
    data: restaurants,
    error,
    isPending,
  } = useRestaurants({ state: state.short, city: city.name })

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">Error loading restaurants: </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen noScroll>
      <FlatList
        data={restaurants}
        renderItem={({ item: restaurant }) => (
          <Button
            onPress={() => {
              navigation.navigate("RestaurantDetails", {
                slug: restaurant.slug,
              })
            }}
          >
            {restaurant.name}
          </Button>
        )}
        keyExtractor={(item) => item._id}
      />
    </Screen>
  )
}

export default RestaurantList

```

✏️ Update **src/screens/RestaurantDetails/RestaurantDetails.tsx** to be:

```
import { useNavigation } from "@react-navigation/native"
import { StackScreenProps } from "@react-navigation/stack"
import Loading from "@shared/components/Loading"
import RestaurantHeader from "@shared/components/RestaurantHeader"
import Box from "@shared/design/Box"
import Button from "@shared/design/Button"
import Screen from "@shared/design/Screen"
import Typography from "@shared/design/Typography"
import { useRestaurant } from "@shared/services/pmo/restaurant"
import { useEffect } from "react"

import { RestaurantsStackParamList } from "../../App"

export interface RestaurantDetailsProps
  extends StackScreenProps<RestaurantsStackParamList, "RestaurantDetails"> {}

const RestaurantDetails: React.FC<RestaurantDetailsProps> = ({ route }) => {
  const { slug } = route.params
  const navigation = useNavigation()
  const { data: restaurant, error, isPending } = useRestaurant({ slug })

  useEffect(() => {
    if (restaurant) {
      navigation.setOptions({ title: `${restaurant.name}` })
    }
  }, [restaurant, navigation])

  if (error) {
    return (
      <Screen>
        <Box padding="m">
          <Typography variant="heading">
            Error loading restaurant details:{" "}
          </Typography>
          <Typography variant="body">{error.message}</Typography>
        </Box>
      </Screen>
    )
  }

  if (isPending) {
    return <Loading />
  }

  return (
    <Screen>
      <RestaurantHeader restaurant={restaurant} />
      <Button onPress={() => console.warn("Place an order")}>
        Place an order
      </Button>
    </Screen>
  )
}

export default RestaurantDetails

```

## Next steps

Next, we will learn how to [Handling User Inputs](https://www.bitovi.com/academy/learn-react-native/user-inputs.html?hsLang=en-us) in React Native applications.

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