###### tags: `React` `state` `props`
# [week 21] 前端框架 - 先別急著學 React
> 本篇為 [[FE302] React 基礎 - hooks 版本](https://lidemy.com/p/fe302-react-hooks) 這門課程的學習筆記。如有錯誤歡迎指正!
```
學習目標:
P1 我知道 React 的目的以及原理
P1 我知道我們為什麼需要 React
P1 我知道使用 React 跟之前使用 jQuery 的區別
P1 我理解 state 跟 props 的不同
```
---
## 先別急著學 React
在開始之前,我們先來複習如何用 jQuery 來做出一個簡單的 Todo List 吧!
1. 以下是套用 Bootstrap 切的版型,不過切版部分不是這次要探討的重點:
```htmlmixed=
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
```
<iframe height="324" style="width: 100%;" scrolling="no" title="[week21] Todo List template" src="https://codepen.io/heidiliu2020/embed/preview/vYKoNJK?height=324&theme-id=dark&default-tab=html,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href='https://codepen.io/heidiliu2020/pen/vYKoNJK'>[week21] Todo List template</a> by Heidi-Liu
(<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>.
</iframe>
2. 接著引入使用 jQuery:
```javascript=
<script src="http://code.jquery.com/jquery-latest.js"></script>
```
3. 就可以來新增功能了:
- 新增
- 切換已完成/未完成
- 刪除
<iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template" src="https://codepen.io/heidiliu2020/embed/preview/vYKoNJK?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href='https://codepen.io/heidiliu2020/pen/vYKoNJK'>[week21] Todo List template</a> by Heidi-Liu
(<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>.
</iframe>
## 初探 Component
根據 [React](https://zh-hant.reactjs.org/docs/components-and-props.html) 官網定義:
> Component 使你可以將 UI 拆分成獨立且可複用的程式碼,並且專注於各別程式碼的思考。
簡單來說,component(元件、組件)其實就像 JavaScript 的 function,可接收任意的 props(參數, property 的簡寫)並且回傳描述畫面的 React element。
以我們剛才實作的 todolist 來說,就可以根據功能分成不同的 component:
![](https://i.imgur.com/XYSTyfM.png)
### Component & Props
再來我們可試著用 component 的概念改寫程式碼,例如切割出 Todo 和 Button component。
1. 必須用英文大寫開頭命名來辨別是 component:
```javascript=
// Todo component: 用來回傳 html 內容, 用大寫英文開頭
function Todo(content) {
return `
<li class="todo list-group-item d-flex justify-content-between align-items-center">
<div class="todo-title ">${content}</div>
<div class="btn-group">
${Button('btn-undone btn-outline-secondary', '未完成')}
${Button('btn-delete btn-outline-danger', '刪除')}
</div>
</li>
`
}
// Button component
function Button(className, content) {
return `
<button class="btn ${className}" type="button">${content}</button>
`
}
```
2. 監聽事件就可改成 Button component 寫法,比起原本的 HTML 程式碼,用 component 寫法會更容易閱讀:
```javascript=
$('.todos').on('click', '.btn-undone', e => {
const todo = $(e.target).parent();
e.target.remove();
todo.prepend(
Button('btn-done btn-outline-success', '已完成')
);
});
$('.todos').on('click', '.btn-done', e => {
const todo = $(e.target).parent();
e.target.remove();
todo.prepend(
Button('btn-undone btn-outline-secondary', '未完成')
);
});
```
3. 但其實這個寫法還能再做優化,像是 Button() 如果要再傳入更多參數,就會很難快速判斷。因此我們可以改成傳入物件,也就是 props(參數)來簡化:
```javascript=
// Todo component: 傳入物件作為參數
function Todo(props) {
return `
<li class="todo list-group-item d-flex justify-content-between align-items-center">
<div class="todo-title ">${props.content}</div>
<div class="btn-group">
${Button({
className: 'btn-undone btn-outline-secondary',
content: '未完成'
})}
${Button({
className: 'btn-delete btn-outline-danger',
content: '刪除'
})}
</div>
</li>
`
}
// Button component: 傳入物件作為參數
function Button(props) {
return `
<button class="btn ${props.className}" type="button">${props.content}</button>
`
}
```
監聽事件也同樣以物件的形式修改,完整程式碼與執行畫面如下:
<iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template_component" src="https://codepen.io/heidiliu2020/embed/preview/dypbKLw?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href='https://codepen.io/heidiliu2020/pen/dypbKLw'>[week21] Todo List template_component</a> by Heidi-Liu
(<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>.
</iframe>
透過這種寫法,我們能夠將重複的區塊變成模板,建立統一的規則來封裝,也會提高程式碼的可讀性。
但 component 其實是一種抽象的概念,還有另外一種較為標準的寫法 [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components),類似建立新的 HTML 標籤來使用,只是目前瀏覽器支援度還不高,這部分我們並不進行討論。
## 資料 vs 畫面(UI)
根據前面的範例,是把資料放到畫面上來呈現,也就是在新增、修改 todo 時,同時去更動資料和畫面。因此我們可從 UI 去抽取資料,但我們無法直接把 HTML 程式碼存到資料庫,而是要去除標籤,轉換成 JSON 等最原始的資料形式。
此外,透過這種方式其實有個壞處,假如有部分資料沒有更動到,就可能導致資料與畫面不一致。
![](https://i.imgur.com/TKkUPnR.png)
至於要如何解決這個問題,我們可以改成直接修改資料,然後畫面從資料產生,即可保持資料與畫面的一致性。
![](https://i.imgur.com/k1STU0n.png)
這其實就是 React 第二個重要概念 State,畫面永遠由 state 產生。用數學式可表示成 `UI = f(state)`,代表 state 不變的話,透過 state 產生的 UI 也不會改變。
## 畫面永遠都由 state 產生
根據 [React](https://zh-hant.reactjs.org/docs/react-component.html#state) 官網定義:
> State 包含了某個 component 內特定的、會隨時間改變的資料,這個 state 是由使用者定義的。它應是一個簡單的 JavaScript object。
> 如果某個值並沒有在 render 或資料流中被使用(例如計時器的 ID),你不需要將它放在 state 內。
繼續舉剛才的範例,我們可以把 todos 放到 state 物件中,並且將 Todo component 中的 todo 加上 id:
```javascript=
let id = 0;
let state = {
todos: []
}
function Todo(props) {
// 自訂屬性通常以 data- 開頭
return `
<li class="data-id="${id}">
...
`
```
- 新增 todo:透過 updateState() 來更新資料
```javascript=
$('.btn-add').click(() => {
const content = $('.input-todo').val();
if (!content) return;
$('.input-todo').val('');
// 更新 state
updateState({
todos: [...state.todos, {
id,
content,
isDone: false
}]
});
id++;
});
```
- 呼叫 updateState() 來更新資料,再根據資料 render 出畫面:
```javascript=
// 更新 state
function updateState(newState) {
state = newState;
render();
}
function render() {
// 先把畫面清空
$('.todos').empty();
$('.todos').append(
// 把每個 todo 的 HTML 集合起來放到畫面上
state.todos.map(todo => Todo(todo)).join('')
);
}
```
- 刪除 todo:透過篩選掉資料中相對應的 todo 來更新 state
```javascript=
$('.todos').on('click', '.btn-delete', e => {
// 讀取該 todo id
const id = Number($(e.target).parents('.todo').attr('data-id'));
// 更新 state: 篩選掉資料中相對應的 todo
updateState({
todos: state.todos = state.todos.filter(todo => todo.id !== id)
});
});
```
修改 todo 狀態也是類似概念,完整程式碼如下:
<iframe height="265" style="width: 100%;" scrolling="no" title="[week21] Todo List template_component & state" src="https://codepen.io/heidiliu2020/embed/preview/wvzvBbY?height=265&theme-id=dark&default-tab=js,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href='https://codepen.io/heidiliu2020/pen/wvzvBbY'>[week21] Todo List template_component & state</a> by Heidi-Liu
(<a href='https://codepen.io/heidiliu2020'>@heidiliu2020</a>) on <a href='https://codepen.io'>CodePen</a>.
</iframe>
---
## 結論
在剛接觸 React 時,覺得和之前實作的 [SPA 留言版](https://hackmd.io/@Heidi-Liu/note-be101-php-todolist)概念很類似,以非同步方式串接後端 API,透過將前後端分離,以動態方式來更新頁面,同樣是直接更新後端資料來顯示前端畫面。
我們也從 Todo List 範例中,學到 React 最重要的兩個概念 Component 和 State:
- 透過 Component 將畫面與功能模組化,並以傳入的參數 props 來設定屬性或是資料
- 因為畫面永遠由 State 產生,我們會直接更改資料,再由資料去顯示畫面
瞭解到這些概念後,我們就能正式入門 React 這套 JavaScript Library。