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