# React(MRWR)第 8 節: Communication Using the Context System
> Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/)
`20230828Mon.~20230908Fri.`
:::danger
8-138 物件key value相同時縮寫
:::
## 8-130. Introducing Context
老師提到這個章節將延續上個章節,我們將透過把程式碼重構,學習一些新的React的特色,上一章節的程式碼並沒有不好,我們只是要透過重構讓程式碼不同,藉以學習新東西。
以下為原先的作法,會發現editBookId以及deleteBookId在許多元件中重複出現,甚至沒用到這兩個function的元件中也出現了這兩個function,這件事並沒有所謂的錯誤,但是很容易出錯!也會看起來很雜亂,導致難以維護。

因此這章節要談論的就是名為"context"的東西。
> 可以參考官方文件:[Passing Data Deeply with Context](https://react.dev/learn/passing-data-deeply-with-context)
而取代上圖的作法則如下圖所示:

老師提到要使用context大致上分成三個步驟(如下圖),不過事實上會比這圖中三個步驟更加複雜QQ

<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">Step1. Create the context</h3>
</div>
我們建立一個新的檔案,在這個新檔案中我們需要import createContext:
```javascript!
import { createContext } from "react";
```
之後將createContext賦值給一個context物件,以這裡為例,我們建立一個context物件叫做"BookContext",而這個物件中包含兩個properties(屬性)分別叫做"provider"(直翻為提供者)、"consumer"(直翻為消費者),這兩者都屬於component(元件)。

若我們想使用"provider"元件,則寫:
```javascript!
<BookContext.Provider />
```
<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">Step2. Specify(指定) the data that will be shared</h3>
</div>
目前看起來是如果想要分享某個資料(以下圖為例分享的資料為數字5),首先會用上step1的provider component,接著會用一個特殊的prop叫做"value"來分享這個data,而賦予給value的值即我們想分享的東西,想要分享出去的東西可以是數字、字串、物件等等都可以!
而如下圖所示,Provider的子元件MyComponent以及MyComponent的子元件們皆可以使用到這個數字5了!
不過如果想使用這個context,要記得一定要放在Provider這個元件裡面。

<div style="border: black 1px solid; padding: 5px 5px; margin: 15px 0px">
<h3 style="margin: 0">Step3. Consume the data in a component</h3>
</div>
接著我們要使用context中的data,會需要另一個React的function叫做"useContext",所以要import進來,除此之外也要把剛剛step1建立的context物件一起import進到想使用這個context的檔案中:
```javascript!
import { useContext } from "react";
import BookContext from "./book";
```
而useContext()會接收context物件,回傳的內容會是我們在step2時建立的prop的值,也就是數字5,這也是底下圖中的num變數會是5。

****
## 8-132. Changing Context Values
我們透過context,可以在元件需要該data時才去取用,而不用像prop system一樣,必須一層一層傳下去給需要的元件。
但是現在出現了一個問題,所有變動的資料都取決於名為"value"的prop裡面,但目前我們可以看到他就是個數字5,是固定不變的數字。

但是我們希望這個value可以做改變,而做改變就得去rerender畫面,提到rerender勢必就又要提到state了!
所以我們把傳入value的data改為一個物件,這個物件裡面包含一個count變數,以及一個incrementCount()函式。且這個物件data,就如同之前的數字5一樣,任何元件(component)都可以取得這個data。


****
## 8-133. More on Changing Context
我們將針對第七章的程式碼去做重構,主要會變動到兩個檔案,以及新增一個新檔案。
首先,新增一個資料夾叫做"context",並新增一個檔案叫做"books.js"。

接著要來建立context,所以要import react的語法進來,import createContext之後,就可以使用他建立context物件。
```javascript!
import { createContext, useState } from "react";
const BooksContext = createContext();
export default BooksContext;
```
再來要建立一個component叫"Provider",然後會傳入一個prop叫"children"給Provider元件。(children的作用後面才會提)
在Provider元件中,會利用useState來紀錄變數count的變化,以及一個物件是要用來放入`<BooksContext.Provider value={}></BooksContext>`的value之中的。
一旦放入這個value裡,後續元件只要是`<BooksContext.Provider value={}></BooksContext>`的底下元件(不必是父子層關係),都可以使用這個value裡面的物件內容。
最後直接export {Provider}
**books.js**
```javascript!
import { createContext, useState } from "react";
const BooksContext = createContext();
function Provider({children}){
const [count, setCount] = useState(5);
const valueToShare = {
count: count, //也可以寫count即可
incrementCount: () => {
setCount(count+1);
}
};
return(
<BooksContext.Provider value={valueToShare}>
{children}
</BooksContext.Provider>
)
};
export { Provider }
export default BooksContext;
```
上方export { Provider }用了花括號,主要是因為一個檔案只能有一個default export,但可以有多個named export,而有多個named export的情況下,我們就用「解構」的方法去匯入、匯出 And this is why we use the curly braces
(可參考[完全解析 JavaScript import、export](https://www.casper.tw/development/2020/03/25/import-export/))
再來看到`index.js`這個檔案,我們要把整個Provider元件import到這隻檔案裡面。
然後在root.render中放入Provider元件,包住App元件。前面我們可知Provider元件接收了一個參數prop children,這也代表被Provider元件包住的App元件也可以使用這個prop children。
接著在Provider中還有一個重要的東西"value",我們要把要使用的值透過value來傳送給其他元件,這裡我們要使用的值就是在`books.js`中,建立在Provider function裡的物件"valueToShare"。
**index.js**
```javascript
import "./index.css"
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.js";
import { Provider } from "./context/books";
const el = document.getElementById("root");
const root = ReactDOM.createRoot(el);
root.render(
<Provider>
<App />
</Provider>
);
```
換來到`BookList.js`檔案中,我們要使用在`books.js`中建立的context物件"BooksContext",而要使用context物件,就必須import react的語法"useContext",以及當然也要把建立好的"BooksContext"引入
```javascript
import { useContext } from "react";
import BooksContext from "../context/books";
```
再來在BookList function中使用useContext,那是要使用什麼context呢?我們來複習一下剛剛在`books.js`所寫得valueToShare:
```javascript
const valueToShare = {
count: count, //也可以寫count即可
incrementCount: () => {
setCount(count+1);
}
};
```
valueToShare是一個物件,裡面包含兩個屬性:"count"跟"incrementCount"。這裡我們透過「解構」的方式來寫:
```javascript
const {count, incrementCount} = useContext(BooksContext);
```
接著就可以使用count跟incrementCount了:
```javascript!
import { useContext } from "react";
import BooksContext from "../context/books";
import BookShow from "./BookShow";
function BookList({books, onDelete, onEdit}){
const {count, incrementCount} = useContext(BooksContext);
const renderedBooks = books.map((book) => {
return <BookShow onEdit={onEdit} onDelete={onDelete} key={book.id} book={book}/>;
})
return(
<div className="book-list">
{count}
<button onClick={incrementCount}>Click!</button>
{renderedBooks}
</div>
)
}
export default BookList;
```
React好難QQ
****
## 8-134. Application vs Component State
前面知道了context的運作後,再來我們要開始實作書籍的部份。首先的問題就是什麼東西要透過context去做分享呢?

這裡老師將會分成兩種類型的state去判斷是否要使用到context,不過老師有說React還是只有一種state,這裡區分成兩種,只是為了方便我們去思考而已。
這裡用了airbnb作為舉例:

我們可以從上圖發現在"app" 元件中有三個state,在"data picker"元件中各有一個state。而我們可以知道前者的state有眾多的元件都需要,而後者則否,因此如下圖所示,我們可以把前者state區分到Application state,後者則區分為component state(component state也可以將他想作是local state)。

而所謂的application state就是屬於非常適合放到context的state。

接下來看回我們的專案,我們要來觀察state各自屬於什麼類型的state,去把他們分類。(每個人可能有不同的分類法,這沒有一定的答案,不過這裡就是參考老師說的)例如下圖所示:

****
## 8-135. Refactoring to Use Context
我們先回頭看看原先單純用prop system完成的專案關係圖:

不過接下來我們要改成利用context來完成專案,改成如下流程圖:

**books.js**
先把先前設置的count state給刪掉,改成如下程式碼:
```javascript!
import { createContext, useState } from "react";
const BooksContext = createContext();
function Provider({children}){
return(
<BooksContext.Provider value={{}}>
{children}
</BooksContext.Provider>
)
};
export { Provider }
export default BooksContext;
```
**BookList.js**
作法同上,清掉上上章節的count state相關內容:
```javascript!
import { useContext } from "react";
import BooksContext from "../context/books";
import BookShow from "./BookShow";
function BookList({books, onDelete, onEdit}){
const renderedBooks = books.map((book) => {
return <BookShow onEdit={onEdit} onDelete={onDelete} key={book.id} book={book}/>;
})
return(
<div className="book-list">
{renderedBooks}
</div>
)
}
export default BookList;
```
****
## 8-136. Refactoring the App
來到`App.js`檔案中,我們將要如上一章節所說(如下圖),將"Application state"移至provider之中。也就是移動:books、editBookById、deleteBookById、fetchBooks、createBook四者。

移動到我們先前創建的檔案:`./context/books.js`
**books.js**
```javascript!
import { createContext, useState } from "react";
const BooksContext = createContext();
function Provider({children}){
const [books, setBooks] = useState([]);
const fetchBooks = async () => {
const response = await axios.get("http://localhost:3001/books");
setBooks(response.data);
}
const deleteBookById = async (id) => {
await axios.delete(`http://localhost:3001/books/${id}`);
const updatedBooks = books.filter((book) => {
return book.id !== id;
});
setBooks(updatedBooks);
}
const editBookById = async (id, newTitle) => {
const response = await axios.put(`http://localhost:3001/books/${id}`,{
title: newTitle
});
const updatedBooks = books.map((book) => {
if(book.id === id){
return {...book, ...response.data};
}
return book;
})
setBooks(updatedBooks);
}
const createBook = async (title) => {
const response = await axios.post("http://localhost:3001/books",{
title
})
const updatedBooks = [
...books,
response.data
]
setBooks(updatedBooks);
}
return(
<BooksContext.Provider value={{}}>
{children}
</BooksContext.Provider>
)
};
export { Provider }
export default BooksContext;
```
**App.js**
在`App.js`中,唯獨"useEffect"繼續留在`App.js`裡面(目前會報錯,後面才會解釋如何處理)
不過我們會注意到目前的`App.js`中BookCreate跟BookList這兩者元件皆使用prop system取元件(如下所示)。
```javascript!
<BookList onEdit={editBookById} books={books} onDelete={deleteBookById}/>
<BookCreate onCreate={createBook}/>
```
我們現在要使用的話,就必須從context拿,所以先把這些prop全部移除:
```javascript!
<BookList />
<BookCreate />
```
###
接著我們先來完成`App.js`中的"useEffect",思考"useEffect"要如何取得context中的"fetchBooks"

**App.js**
1. 要使用context,第一步肯定要先:
```javascript!
import {useContext} from "react";
```
2. 當然,也要import我們要使用的context(我們先前已經在`books.js`中的最後export default BooksContext了):
```javascript!
import BooksContext from "./context/books";
```
3. 在App()中使用useContext(),並把context物件傳入(context物件即BooksContext)
```javascript!
useContext(BooksContext);
```
4. 而我們要使用BooksContext這個context物件中的"fetchBooks" function,所以我們用解構的寫法來取得"fetchBooks" function
```javascript!
const { fetchBooks } = useContext(BookContext);
```
5. 最後一些小改變,在`App.js`中,我們不再需要更新state,所以可以不用再import {useState} from "react",以及也不需要import axios from "axios"。
不過這兩者我們必須移至`books.js`(也就是專門管理context的檔案)
**books.js**
```javascript!
import { useState } from "react";
import axios from "axios";
```
目前大致把`App.js`給整理好,不過我們的網頁目前是無法運行的,還需要後續的修正。
****
## 8-138. Reminder on Sharing with Context
我們現在看回`books.js`,所有我們希望分享給其他元件的內容,都必須放到provider的value之中,如此才能讓其他子元件使用。
可以看見我們目前在value中傳入了一個空物件。
```javascript!
return(
<BooksContext.Provider value={{}}>
{children}
</BooksContext.Provider>
)
```
但我們希望可以分享所有的"Application state"(如下五個state),這五個state將會across整個網頁!

但我們並不會一個一個傳進去,而是直接建立一個變數,這裡我們稱這個變數為valueToShare(當然我們一般在做大專案不可能取這樣的名字,只是這樣在這裡可以讓我們更清楚這個變數的用途),而我們將賦予這個變數為一個物件,至於如何使用直接看下方程式碼:
```javascript!
const valueToShare = {
books: books,
editBookById: editBookById,
deleteBookById: deleteBookById,
fetchBooks: fetchBooks,
createBook: createBook
}
```
我們可以發現上方的value、key值都完全相同(完全相同:identical)(if the key is identical to the value),我們可以將上方程式碼濃縮(濃縮:condense)成只使用變數名稱即可(如下所示):
```javascript!
const valueToShare = {
books,
editBookById,
deleteBookById,
fetchBooks,
createBook
}
```
接著,只有把valueToShare放進value即可使用了!
```javascript!
return(
<BooksContext.Provider value={{valueToShare}}>
{children}
</BooksContext.Provider>
)
```
###
再來要修正`BookCreate.js`檔案的內容,以下是原先用prop system的作法:
<img src="https://hackmd.io/_uploads/rJvwx0rAh.jpg" style="width: 50%; margin: 0 20px 30px 30px">
我們要改成用context的方式,如下流程:
<img src="https://hackmd.io/_uploads/H1w2eCHA3.jpg" style="width: 50%; margin: 0 20px 30px 30px">
**BookCreate.js**
1. 同樣地,要先import {useContext}以及BookContext物件進到`BookCreate.js`
```javascript!
import { useState, useContext } from "react";
import BooksContext from "../context/books";
```
2. 取得BookContext物件中我們需要的createBook function即可,這裡同樣利用了解構的方法:
```javascript=
const { createBook } = useContext(BooksContext);
```
3. 原先的BookCreate funtion中我們傳入了prop如下:
```javascript=
function BookCreate({onCreate}){
...
}
```
現在我們不需要這個onCreate了,所以可以直接移除掉:
```javascript=
function BookCreate(){
...
}
```
4. 原先底部呼叫了onCreate():
```javascript=
const handleSubmit = (event) => {
event.preventDefault();
onCreate(title);
setTitle("");
}
```
我們需要移除onCreate(),取而代之的是createBook function:
```javascript=
const handleSubmit = (event) => {
event.preventDefault();
createBook(title);
setTitle("");
}
```
:::warning
這裡詢問了助教物件的相關問題,這章節中老師提到:「value、key值都完全相同時,我們可以將上方程式碼濃縮成只使用變數名稱即可」
Q: In the course, the teacher mentioned that "if the key is the same as the value, we can simplify it by using just the variable name in JavaScript." Is there any documentation about this?
Thank You!
A: Not really, as it's a very minor implementation enhancement. It is briefly mentioned in the Mozilla docs here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#property_definitions

###
到助教提供的網址後,有找到相關的內容,不過真的就是很簡單的提到說我們可以這樣縮短而已:


:::
****
## 8-139. Props and Context Together
這章節要修正BookList.js檔案,以下是原先用prop system的作法:

若改成利用context用法,則如下圖所示:

**BookList.js**
1. 取得BookContext物件中我們需要的books即可,這裡同樣利用了解構的方法:
```javascript=
const { books } = useContext(BooksContext);
```
2. 原先的BookList function接受了一組prop,如下:
```javascript=
function BookList({books, onDelete, onEdit}){
......
}
```
但現在我們不需要傳遞prop了,所以可以整個清掉:
```javascript=
function BookList(){
......
}
```
包刮BookList function裡頭的變數renderedBooks中,原先也運用到了prop,、onEdit及onDelet:
```javascript!
const renderedBooks = books.map((book) => {
return <BookShow onEdit={onEdit} onDelete={onDelete} key={book.id} book={book}/>;
})
```
現在同樣地也將這些屬性移除,不過這裡可以注意到,我們仍然可以保留prop book(因為我還是希望可以讀取到books state中的單本book),從這裡可以知道prop與context是可以同時使用的。
這個book prop將往下傳給BookShow元件使用的。
```javascript!
const renderedBooks = books.map((book) => {
return <BookShow key={book.id} book={book}/>;
})
```
****
## 8-140. Last Bit of Refactoring
接下來在往下看BookShow.js檔案,以下是原先用prop system的作法:

這是改成利用context的流程圖:

**BookShow.js**
1. 想使用context,要先import {useContext}以及BookContext物件進到`BookCreate.js`
```javascript!
import { useState, useContext } from "react";
import BooksContext from "../context/books";
```
2. 取得BookContext物件中我們需要的deletBookById即可,這裡同樣利用了解構的方法:
```javascript!
const { deletBookById } = useContext(BooksContext);
```
3. 有些prop,例如onDelete、onEdit,我們不再接收這些prop(原先寫法):
```javascript!
function BookShow({book, onDelete, onEdit}){
......
}
```
所以改寫成下列程式碼(僅book保留):
```javascript!
function BookShow({book}){
......
}
```
4. 找到原先用onDelte,我們將要用delteBoolById來取代onDelte。
5. 原先在handleSubmit function:
```javascript!
const handleSubmit = (id, newTitle) => {
setShowEdit(false);
onEdit(id, newTitle);
};
```
我們這裡不再需要onEdit function,因為onEdit只有在下一層的元件"BookEdit"會使用到,因此把onEdit刪除之後,也可以一併把id, newTitle兩個參數給移除:
```javascript!
const handleSubmit = () => {
setShowEdit(false);
};
```
###
最後來到BookEdit.js檔案,以下是原先用prop system的作法:

以下是改用context的流程圖:

**BookEdit.js**
1. 使用context,要先import {useContext}以及BookContext物件進到`BookCreate.js`
```javascript!
import { useState, useContext } from "react";
import BooksContext from "../context/books";
```
2. 取得BookContext物件中我們需要的editBookById即可,這裡同樣利用了解構的方法:
```javascript!
const { editBookById } = useContext(BooksContext);
```
3. 看到handleSubmit的函式中,原先的樣子:
```javascript!
const handleSubmit = (event) => {
event.preventDefault();
onSubmit(book.id, title);
};
```
我們改用context後,仍然需要使用到onSubmit()(onSubmit()的用意在於,可以在我們submit表單時,關閉edit的模式,不同的是我們不再需要傳入裡頭那些參數:
```javascript!
const handleSubmit = (event) => {
event.preventDefault();
onSubmit();
};
```
而至於編輯的功能,則由editBookById來處理,所以此時我們再傳入book.id及title即可:
```javascript!
const handleSubmit = (event) => {
event.preventDefault();
onSubmit();
editBookById(book.id, title);
};
```
****
## 8-A Small Taste of Reusable Hooks
我們前面提到了useState、useEffect、useContext,這些都是React給我們的,這類functions我們會稱他們為"hook"。

而除了使用React提供的hooks之外,我們也可以自己客製化hook。

前面幾個小節都可以注意到使用context的話,第一個步驟不外乎就是import兩個東西,一個是useContext、一個是BooksContext。(如下所示)
```javascript!
import { useState, useContext } from "react";
import BooksContext from "../context/books";
```
既然要一直重複這兩件事情,何不另開一個檔案專們處理、專門建立conotext呢?
於是我們可以建立一個新的資料夾叫做"hooks"來管理各種hook(當然這裡目前只會實作一個hook)

這裡將使用BookList.js來做示範,先來看看原先的程式碼長什麼樣子:
**BookList.js**
```javascript!
import { useContext } from "react";
import BooksContext from "../context/books";
import BookShow from "./BookShow";
function BookList(){
const { books } = useContext(BooksContext);
const renderedBooks = books.map((book) => {
return <BookShow key={book.id} book={book}/>;
})
return(
<div className="book-list">
{renderedBooks}
</div>
)
}
export default BookList;
```
修改後的樣子,我們把重複的內容寫到./hooks/use-books-context.js檔案裡面
**use-books-context.js**
```javascript!
import { useContext } from "react";
import BooksContext from "../context/books";
function useBooksContext(){
return useContext(BooksContext);
}
export default useBooksContext;
```
**BookList.js**
```javascript!
import BookShow from "./BookShow";
import useBooksContext from "../hooks/use-books-context";
function BookList(){
const { books } = useBooksContext();
const renderedBooks = books.map((book) => {
return <BookShow key={book.id} book={book}/>;
})
return(
<div className="book-list">
{renderedBooks}
</div>
)
}
export default BookList;
```