# 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 做操作!為了保護資訊安全以及效能

像是 JS 這種 browser-side 的語言是很容易洩漏資訊安全相關的東西,舉例:金鑰,因為他們是很容易可以在網頁中被取得以及參閱的,只要打開開發者工具就可以很輕易地看到每個部分的程式碼
所以 React 會跟後端語言撰寫的 App 去接觸 database 因為後端的扣不會被使用者看到,相關的資訊會更為安全
# 程式範例

利用 fetch Movie 按鈕來取得 電影資料 並呈現在下方的 moveList
透過這個 [starwar API](https://swapi.dev/),裡頭有一些寫死的資料可以送 GET request 來操作,要說明的是這個網頁裡面的東西是後端的 app 並不是 database ,如同前面解釋的一般
這邊提到 http request API 多半是再說 REST API 或是 GraphQL API

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

# 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;
```
成功後就可以順利把資料串接到網頁上摟!

# 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 的錯誤

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>
);
};
```
對應的表格

當點擊 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 儲存檔案的位置,即可以接收到已經儲存進去的物件

但是這邊要針對拿回來的資料做一點處理,因為已經不是 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,
})
}
```

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