---
# System prepended metadata

title: React - Sending http requests (connecting to a database)
tags: ['Javascript, React']

---

#  React - Sending http requests (connecting to a database)

###### tags: `Javascript, React`

# 這個 module 在說什麼？

* How do React interact with database?
* Sending http requests & Using Responses
* Handling Errors & Loaing state

# 如何連接到 Databases?

> browser-side 的扣 (也就是 React) 不會直接跟 database 做操作！為了保護資訊安全以及效能

![](https://i.imgur.com/oHRXCr4.png)

像是 JS 這種 browser-side 的語言是很容易洩漏資訊安全相關的東西，舉例：金鑰，因為他們是很容易可以在網頁中被取得以及參閱的，只要打開開發者工具就可以很輕易地看到每個部分的程式碼

所以 React 會跟後端語言撰寫的 App 去接觸 database 因為後端的扣不會被使用者看到，相關的資訊會更為安全

# 程式範例

![](https://i.imgur.com/XsvZJpI.png)

利用 fetch Movie 按鈕來取得 電影資料 並呈現在下方的 moveList 

透過這個 [starwar API](https://swapi.dev/)，裡頭有一些寫死的資料可以送 GET request 來操作，要說明的是這個網頁裡面的東西是後端的 app 並不是 database ，如同前面解釋的一般

這邊提到 http request API 多半是再說 REST API 或是 GraphQL API

![](https://i.imgur.com/ZmLs2Ij.png)

大概來說就是使用不同的 entrypoint 可以得到不同的 result 

![](https://i.imgur.com/lnCP3Z4.png)

# Sending a GET Request

這邊會介紹如何從 React App 送 http request 到 backend 

## App.js

主要透過 fetchMoviesHandler 函式來觸發整個 http request 發送

```javascript=
import React, { useState } from "react";

import MoviesList from "./components/MoviesList";
import "./App.css";

function App() {
  const [movies, setMovies] = useState([]);

  function fetchMovieHandler() {
    fetch("https://swapi.dev/api/films") // 這邊 backend 的位置
      .then((response) => {
        return response.json(); // 這邊解析回傳的 json 轉換回物件
      })
      .then((data) => {
        const transformedMovies = data.results.map((movieData) => {
          return {
            id: movieData.episode_id,
            title: movieData.title,
            openingText: movieData.opening_crawl,
            releaseDate: movieData.release_date
          };
        }); // 這邊因為接口使用的內容不同所以要從 backend 回傳的資料中擷取需要的部分也就是 id, title, openingText, releaseDate 

        setMovies(transformedMovies); // 並且把轉化好的 data 更新 state 
      });
  }

  return (
    <React.Fragment>
      <section>
        <button onClick={fetchMovieHandler}> Fetch Movies </button>
      </section>
      <section>
        <MoviesList movies={movies} />
      </section>
    </React.Fragment>
  );
}

export default App;
```
## MoviesList.js

這邊是接受 movieList 的接口的檔案

```javascript=
import React from 'react';

import Movie from './Movie';
import classes from './MoviesList.module.css';

const MovieList = (props) => {
  return (
    <ul className={classes['movies-list']}>
      {props.movies.map((movie) => (
        <Movie
          key={movie.id}
          title={movie.title}
          releaseDate={movie.releaseDate}
          openingText={movie.openingText}
        />
      ))}
    </ul>
  );
};

export default MovieList;
```
成功後就可以順利把資料串接到網頁上摟！

![](https://i.imgur.com/mX5WD5i.png)

# Using async/ await

主要把 .then 改為使用 async/ await 的方式讓扣更好閱讀

```javascript=
async function fetchMovieHandler() {
    const response = await fetch("https://swapi.dev/api/films");
    const data = await response.json();
    
    const transformedMovies = data.results.map((movieData) => {
      return {
        id: movieData.episode_id,
        title: movieData.title,
        openingText: movieData.opening_crawl,
        releaseDate: movieData.release_date,
      };
    });

    setMovies(transformedMovies);
  }
```

# Handling Loading & Data States

在正常使用情境中，當使用者點擊 Fetch Movies 時，應該會有一些 loading 圖示或是文字來告知使用者，事件有被正確的觸發

於是多設置了一種狀態 isLoading 來確認是否是 Loading 狀態

```javascript=
const [isLoading, setIsLoading] = useState(false);
```
這邊處理了三種 state 分別是

1. 沒有在 loading 並且 有找到 movie 於是 MoveList 就跑出來
1. 沒有在 loading 並且 movie 沒有找到 於是顯示 p tag 沒找到電影
1. 有在 loading 就直接顯示 p tag 文字 Loading 

```javascript=
<React.Fragment>
      <section>
        <button onClick={fetchMovieHandler}> Fetch Movies </button>
      </section>
      <section>
        {!isLoading && movies.length > 0 && <MoviesList movies={movies} />}
        {!isLoading && movies.length === 0 && <p>Found no movies.</p>}
        {isLoading && <p>Loading...</p>}
      </section>
</React.Fragment>
```

# Handling Http Errors

處理 http 回來的 errors 基本上都是一些連線上面出現的問題，但是如果出了問題畫面只會呈現 loading 的話並不是一個很好的使用者體驗因此這邊要處理 http 回傳的錯誤告知使用者目前的狀態

這邊我把要 fetch 的網址故意打錯製造一個 http 會錯誤的情境來做範例：

網址應該是 films ，因此 console 印出了 404 的錯誤

![](https://i.imgur.com/43CjTD8.png)

fetch 的抓取 http 錯誤的寫法是使用 try, catch 

* try 把 fetch 的整個內容放進去看有沒有錯
* catch 有錯的話在這邊做處理出錯的話要做什麼事

這邊關鍵的錯誤訊息來自 throw 內的字串

response.ok 這個 promise 的屬性會回傳 true/ false 可以用來判斷是否連線成功

如果連線狀態失敗則丟出自訂的錯誤訊息，並且會被 catch 抓到 error 其屬性的 message 呈現到網頁上，要注意的是這邊抓取 response.ok 的狀態必須要擺在 response.json() 之前，因為如果位置是 reponse.json() 先觸發的話則會是另一個錯誤跟 josn parse 有關的錯誤，後面的程式碼就不會執行了

```javascript=
async function fetchMovieHandler() {
    try {
      setIsLoading(true);
      const response = await fetch("https://swapi.dev/api/film");

      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

      const data = await response.json();

      const transformedMovies = data.results.map((movieData) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_date,
        };
      });

      setMovies(transformedMovies);
    } catch (error) {
      setError(error.message);
    }

    setIsLoading(false);
  }
```
這邊把 throw 的 error 使用判斷丟上畫面

```javascript=
<section>
        {!isLoading && movies.length > 0 && <MoviesList movies={movies} />}
        {!isLoading && movies.length === 0 && !error && <p>Found no movies.</p>}
        {!isLoading && error && <p>{error}</p>}
        {isLoading && <p>Loading...</p>}
</section>
```

判斷式都寫在 JSX 覺得不太乾淨嗎？ 讓我們改在邏輯中吧！

最後 JSX 就只剩下 content 摟！

```javascript=
let content = <p>found no movies.</p>;

  if (movies.length > 0) {
    content = <MoviesList movies={movies}></MoviesList>;
  }

  if (error) {
    content = <p>{error}</p>;
  }
  
  if (isLoading) {
    content = <p>Loading...</p>;
  }
```

# Using useEffect() For Requests

這邊想要做到一進入頁面不按按鈕就直接 fetch 到電影資料，最簡單的方式就是透過 useEffect 並且不放入 dependencies 讓他一進入頁面跑一次 fetch 電影資料，不過放入 dependencies 會是更注重效能的寫法

```javascript=
 useEffect(() => {
    fetchMovieHandler();
  }, [fetchMovieHandler])
```

但會出現一個問題，fetchMovieHandler 是一個函式也就是物件，所以在每一次 re-executed , re-evaluated 後都會指向一個新的位置導致畫面再次 re-executed , re-evaluated 也就是會造成 infinte loop ，所以 useCallback 的使用可以記憶起 fetchMovieHandler 函式，讓它不會一直刷新

useCallback 的 dependencies 並沒有對外使用的 state 所以為空

記得要把 async 加回去匿名函式

```javascript=
const fetchMovieHandler =  useCallback(async () => {
    try {
      setIsLoading(true);
      const response = await fetch("https://swapi.dev/api/films");

      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

      const data = await response.json();

      const transformedMovies = data.results.map((movieData) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_date,
        };
      });

      setMovies(transformedMovies);
    } catch (error) {
      setError(error.message);
    }

    setIsLoading(false);
  },[]);
```

# Sending a POST Request

我們會在操作一個表格來新增 movie 的資料到 firebase 的 realtime 資料庫

movie 物件的內容

```javascript=
const Movie = (props) => {
  return (
    <li className={classes.movie}>
      <h2>{props.title}</h2>
      <h3>{props.releaseDate}</h3>
      <p>{props.openingText}</p>
    </li>
  );
};
```
對應的表格

![](https://i.imgur.com/YzaQV18.png)

當點擊 Add Movie 後，會送資料進去 firebase 儲存，整個把 movie 推上去的過程會在下方函式中呈現 使用 POST 方法來達成

```javascript=
function addMovieHandler(movie) {
    fetch();
  }
```

這邊是寫 POST 的函式的寫法

```javascript=
async function addMovieHandler(movie) {
    const response = await fetch('https://react-http-b7d01-default-rtdb.asia-southeast1.firebasedatabase.app/movies.json', {
      method: 'POST', // 預設是使用 GET
      body: JSON.stringify(movie), // 把物件 movie 轉成 json 格式
      headers: {
        'Content-type':'application/json' // 算是格式需求，雖然 firebase 不用，但為了以後需求先練習寫
      }
    });

    const data = response.json();

    console.log(data);
  }
```

接下來修改 fetchMoviesHandler 的 url 改為 firebase 儲存檔案的位置，即可以接收到已經儲存進去的物件

![](https://i.imgur.com/XwT1qTG.png)

但是這邊要針對拿回來的資料做一點處理，因為已經不是 array 所以要把 map 改成使用 for...in 來迭代資料進去 `setMovies()`

```javascript=

const loadedMovies = [];

for( const key in data ) {
        loadedMovies.push({
          id: key,
          title: data[key].title,
          openingText: data[key].openingText,
          releaseDate: data[key].releaseDate,
        })
      }
```

![](https://i.imgur.com/H1LWjAv.png)


最後把 loadedMovies 帶入 `setMovies()` 即可 fetch 到 firebase 內我們 POST 上去的內容摟！

![](https://i.imgur.com/nS4PPYe.png)
