###### tags: `React` `state` `hooks` `useState` `useRef`
# [week 21] React Hooks API:useState & 再戰 Todo List
> 本篇為 [[FE302] React 基礎 - hooks 版本](https://lidemy.com/p/fe302-react-hooks) 這門課程的學習筆記。如有錯誤歡迎指正!
```
學習目標:
P1 我知道 React 的目的以及原理
P1 我知道我們為什麼需要 React
P1 我知道使用 React 跟之前使用 jQuery 的區別
P1 我理解 state 跟 props 的不同
```
---
## React Hooks API
根據[官網介紹](https://zh-hant.reactjs.org/docs/hooks-intro.html?no-cache=1):
> Hook 是 React 16.8 中增加的新功能。它讓你不必寫 class 就能使用 state 以及其他 React 的功能。
先前提到,React 的核心概念之一是 component,而根據寫法又可分為兩種:
- class compent:透過 class 寫成,可以控制 state 和生命週期
- function compent:透過 function 寫成,主要用來呈現 UI
### hook 其實就是 function
根據[官方文件](https://zh-hant.reactjs.org/docs/hooks-overview.html):
> hook 是 function,讓我們可以從 function component「hook into」React state 與生命週期功能。
也就是說,React 因為加入了 hooks,再結合 props、state、context、refs 以及 lifecycle 等概念,提供了更直接的 API 使用,讓我們能在 functional component 管理狀態和使用生命周期等功能,藉此簡化程式碼與提高重用性。
引入 hook 的語法如下,原本要寫成 React.useState,透過 ES6 解構語法即可直接使用 useState:
```javascript=
import { useState, useEffect, useRef } from 'react';
```
### hooks 只能寫在 component 第一層
因為 React 背後的機制,hooks 只能寫在 component 第一層。
也就是說,不能把 hooks 寫在 if - else 等條件判斷句裡面,像是符合 XX 條件才使用,否則會出現錯誤訊息:
> React Hook "useRef" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
只能自行在 hooks 裡面進行判斷是否使用,例如下方寫法:
```javascript=
useEffect(() => {
// 若不符合就直接 return
if (!todos) return;
// 若符合要做的事情
})
```
### useState:用來設定 react 中的 state
語法如下:
```javascript=
const [currentValue, setCurrentValue] = useState(initialValue);
```
- currentValue:存放 state 的值
- setCurrentValue:用來設定 state 值
- initialValue:state 的初始值
還記得我們前面舉的 Counter component 範例嗎?簡單來說,就是在 `useState([初始值])` 傳入初始值,然後回傳一個陣列:
```javascript=
function App(){
const [todos, setTodos] = useState([1])
}
```
其中 todos 具有 immutable 特性,也就是無法再賦值,必須用 setTodos 來產生新的 state,才能在每次 render 畫面時改變 todos 的值。
透過這個概念,我們也能實作出簡單的 Todo List:
```javascript=
// 解構語法
import { useState } from 'react';
function App() {
const [todos, setTodos] = useState([
1
]);
const handleButtonClick = () => {
// 傳入參數: 新的 todo + 解構 todos
setTodos(["new todo", ...todos]);
}
return (
<div className="App">
/* 在 JSX 中,單標籤必須 /> 結尾 */
<input type="text" placeholder="Add todo..." />
<button onClick={handleButtonClick}>Add Todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} /> )
}
</div>
);
}
```
![](https://i.imgur.com/f6EsvhC.png)
### controlled vs uncontrolled
> 詳細可參考[官方文件](https://zh-hant.reactjs.org/docs/forms.html#controlled-components)。
在 React 中,表單元素的處理可分為 uncontrolled 和 controlled,兩者之間的差別,在於 component 的資料是否受到 React 的控制:
- uncontrolled component:資料不受 React 的控制
- 例如 input、textarea 等表單元素,通常會維持本身的 state,並根據使用者的輸入來更新該元素的 state
- 若想取得 uncontrolled component 的值,可透過直接操作 DOM 或使用 useRef 來選取特定元素
- controlled component:資料受到 React 的控制
- 如果將資料的控制權交給 React 來處理,畫面就會根據 state 是否改變來重新渲染
參考文章:
- [[Day 27 - 即時天氣] React 中的表單處理(Controlled vs Uncontrolled)以及 useRef 的使用](https://ithelp.ithome.com.tw/articles/10227866)
---
## 再戰 Todo List
### 新增 Todo
透過下方程式碼,可存取 input 的值,並藉由 setValue 來更新 todos 狀態:
```javascript=
import TodoItem from './TodoItem'
// 解構語法
import { useState, useRef } from 'react';
function App() {
const [todos, setTodos] = useState([
1
]);
const [value, setValue] = useState('');
const handleButtonClick = () => {
// 傳入參數: 新的 todo + 解構 todos
setTodos([value, ...todos]);
// 新增完 todo 後清空 value
setValue('');
}
const handleInputChange = (e) => {
// 拿到 input 的 value
setValue(e.target.value);
}
return (
<div className="App">
<input type="text" placeholder="Add todo..." value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add Todo</button>
{
todos.map((todo, index) => <TodoItem key={index} content={todo} /> )
}
</div>
);
}
```
![](https://i.imgur.com/EAY1tzO.png)
### useRef:用來抓取 DOM 節點的 hook
基本用法:
```javascript=
const refContainer = useRef(initialValue);
```
根據[官方文件](https://zh-hant.reactjs.org/docs/hooks-reference.html#useref)介紹:
> useRef 回傳一個可變的 ref object,其 .current 屬性被初始為傳入的參數(initialValue)。回傳的 object 在 component 的生命週期將保持不變。
簡單來說,useRef 是可持有 mutable(可變的)值、具有 .current 屬性的「盒子」,並具有以下特性:
- 當 .current 屬性有變動時不會觸發重新 render
- 在每次 render 時都會給同一個的 ref object
我們可透過宣告 `id = useRef(2)`,就能夠在每次 render 時,更改每個 todo 中 id.current 的值:
```javascript=
function App() {
const [todos, setTodos] = useState([
{id: 1, content: 'todo1'}
]);
const [value, setValue] = useState('');
// useRef: 能我們抓取到 DOM 節點的 hooks
// 會回傳一個物件,要以 id.current 讀取
const id = useRef(2);
const handleButtonClick = () => {
setTodos([
{
id: id.current,
content: value
}, ...todos]
);
setValue('');
id.current++;
}
const handleInputChange = (e) => {
setValue(e.target.value);
}
return (
<div className="App">
<input type="text" placeholder="Add todo..." value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add Todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} /> )
}
</div>
);
```
傳入 todo 這個參數,並放到 TodoItem:
```javascript=
export default function TodoItem({ content, todo }) {
return (
<TodoItemWrapper data-todo-id={todo.id}>
<TodoContent>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>未完成</Button>
<RedButton>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
===上方寫法等同於===
function TodoItem() {
// ...
}
export default TodoItem;
```
### 刪除 todo
#### 1. 把要做的 function 寫在 Parent,並傳入參數給 Children
把 handleDeleteTodo 這個 function 當作 props 傳給 TodoItem:
```javascript=
const handleDeleteTodo = id => {
}
return (
<div className="App">
<input type="text" placeholder="Add todo..." value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add Todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} /> )
}
</div>
);
```
#### 2. 再由 Children 呼叫 function
TodoItem 就可以接收這個 funtcion,並透過刪除按鈕的監聽事件,來呼叫 handleDeleteTodo 以及回傳該 `todo.id`:
```javascript=
export default function TodoItem({ content, todo, handleDeleteTodo }) {
return (
<TodoItemWrapper data-todo-id={todo.id}>
<TodoContent>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>未完成</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
```
#### 3. 接著在 Parent 處理 function
```javascript=
const handleDeleteTodo = id => {
// 若用 splice() 會改到原本的 todo,因此要用 filter(),留下該 id 以外的 todo
setTodos(todos.filter(todo => todo.id !== id))
}
```
### 編輯 todo
在實作編輯 todo 之前,要先來檢視資料結構,也就是把 todo 的狀態加上 isDone,用來判斷是否已完成:
```javascript=
function App() {
const [todos, setTodos] = useState([
{ id: 1, content: 'done', isDone: true },
{ id: 2, content: 'not done', isDone: false }
]);
const [value, setValue] = useState('');
const id = useRef(3);
```
接著在 TodoItem 的按鈕加上三元運算子,用來判斷 todo 狀態,再藉由 $isDone 這個參數,判斷式是否執行後面的 JSX 語法:
```javascript=
const TodoContent = styled.div`
font-size: 26px;
color: ${props => props.theme.colors.primary_300};
// 若 isDone 這個參數為 true(已完成) 則接續後面的 JSX 語法
${props => props.$isDone && `
text-decoration: line-through;
`}
`
export default function TodoItem({ content, todo, handleDeleteTodo }) {
return (
<TodoItemWrapper data-todo-id={todo.id}>
// 傳入 isDone 這個參數
<TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>
// 透過三元運算子判斷 todo 狀態
{todo.isDone ? '已完成' : '未完成'}
</Button>
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
```
除了三元運算子,也可改寫成邏輯運算子 && 的寫法,適用於多種可能的情況:
```javascript=
<Button onClick={handleToggleClick}>
{todo.isDone && '已完成'}
{!todo.isDone && '未完成'}
</Button>
```
接著實作 handleToggleIsDone 修改 todo 功能:
```javascript=
const handleToggleIsDone = id => {
setTodos(todos.map(todo => {
// 如果不是要修改的 todo id 就直接回傳
if (todo.id !== id) return todo;
// 要修改的 todo id
return {
// todo 原本的東西
...todo,
// 要修改的屬性
isDone: !todo.isDone
}
}));
}
```
傳入參數到 TodoItem:
```javascript=
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone}/> )
}
```
由 TodoItem 接收參數,可以把 click 事件抽出來寫,相較於原本的 inline function,能夠提高程式碼的可讀性:
```javascript=
export default function TodoItem({ todo, handleDeleteTodo, handleToggleIsDone }) {
const handleToggleClick = () => {
handleToggleIsDone(todo.id);
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id);
}
return (
<TodoItemWrapper data-todo-id={todo.id}>
<TodoContent $isDone={todo.isDone}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isDone ? '已完成' : '未完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper>
);
}
```
### Transient props:`$<props>`
在上方程式碼中,加在 TodoContent 的 $isDone 這個 props,會被視為 style component props,不會被繼續傳到下一個 DOM 元素,也就不會顯示在 TodoContent 標籤上。
如果沒有加上 $ 符號,這個 props 就會被直接加在 TodoContent 這個 DOM 結構上。
再以下方程式碼為例:
```javascript=
<TodoContent id="abc" $isDone={todo.isDone}>{todo.content}</TodoContent>
```
可以發現經過 render 之後,在 DOM 元素只會出現 `id="abc"` 這個屬性,而不會有 `$isDone`,這是因為 Transient props 不會被往下傳:
![](https://i.imgur.com/K4ROwak.png)
## Todo List 總結
透過實作簡單的 Todo List,其實我們就差不多學會了有關 React 的基礎:
1. Component 組件
開發 React 很重要的一點,就是去思考在頁面有哪些重複性高或相似的 Element,再透過 JSX 將這些 Element 建立成一個 Components,讓每個 Components 擁有重複性及可擴充性。
2. Props 參數
Props 主要提供值給 Component,用來設定屬性或資料,因此就算是同一個組件,也會根據提供的 Props 而有所不同。
3. Style 樣式
可透過幾種方式撰寫 React 中的 CSS,目前主流方法是透過 styled-components 這個套件來撰寫 CSS 語法。
4. Event handler 事件機制
和過去在網頁添加事件監聽不同,必須先以 `querySelector()` 選取 DOM 元素:
```javascript=
function sayHello() {
alert('Hello!');
}
document.querySelector('.sayHello').addEventListener('click', sayHello);
```
而 React 把 DOM 和 JavaScript 程式碼寫在一起,因此可直接在 DOM 元素加上 onClick、onSubmit、onKeyDown 等事件監聽:
```javascript=
function TodoItem({ todo, handleDeleteTodo }) {
const handleDeleteClick = () => {
handleDeleteTodo(todo.id);
}
return (
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
);
}
/* 也可簡化成箭頭函式 */
function TodoItem({ todo, handleDeleteTodo }) {
return (
<RedButton onClick={() => {
handleDeleteTodo(todo.id)
}}>刪除</RedButton>
);
}
```
5. JSX 語法
透過 JSX 語法,即可將 HTML 語法轉成 JavaScript 的形式,讓我們用來建立 React elements。
使用 JSX 時需注意下列幾點:
- class 是保留字,必須改寫成 className
- 可在大括號內寫程式碼,例如:`{ JS code }`,也因此 inline-style 需改為駝峰式命名
- 沒有迴圈的概念,也沒有 if-else 判斷式
- 解決方法:透過三元運算子,或是邏輯運算子 && 進行判斷
此外,當我們要 render 一系列 list 的時候,會使用 `map()` 把資料變成一個陣列,然後 render 需要提供 key:
```javascript=
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} /> )
}
```
6. State 狀態
是 React 中最重要的觀念,可透過 useState 設定 state 初始值,再以 setState 去改變 state。state 會對應到一個 UI,一旦 state 有變動,就會自動呼叫 render()。
state 最基本的語法如下:
```javascript=
const [currentValue, setCurrentValue] = useState(initialValue);
```
在 React 當中,若要進行新增、編輯、刪除功能,雖然有許多方法能夠達成,但基本上有固定的作法:
- 新增功能:解構語法
```javascript=
const handleButtonClick = () => {
setTodos([
{
// 要新增的 todo
id: id.current,
content: value
// 解構語法
}, ...todos]
);
setValue('');
id.current++;
}
```
- 編輯功能:map()
```javascript=
const handleToggleIsDone = id => {
setTodos(todos.map(todo => {
// 如果不是要修改的 todo id 就直接回傳
if (todo.id !== id) return todo;
// 要修改的 todo id
return {
// todo 原本的東西
...todo,
// 要修改的屬性
isDone: !todo.isDone
}
}));
}
```
- 刪除功能:filter()
```javascript=
const handleDeleteTodo = id => {
// 留下該 id 以外的 todo
setTodos(todos.filter(todo => todo.id !== id))
}
```
###
---
## 結論
這和以往的思考模式其實很不一樣,像是在切好的 UI 畫面上新增各種功能;而 React 則是先思考 state 狀態,再去想會如何改變畫面。
記住一個重點,就是 Component 之間可透過 props 把 state 傳遞下去。並且,只要 state 所有變動,就會觸發 render() 來更新 UI 畫面。
參考資料:
- [常見的幾個 React hooks 教學-useState、useEffect、useRef](https://snh90100.medium.com/%E5%B8%B8%E8%A6%8B%E7%9A%84%E5%B9%BE%E5%80%8B-react-hooks-%E4%BB%8B%E7%B4%B9-usestate-useeffect-useref-40c9acd0cc4c)
- [React 性能優化大挑戰:一次理解 Immutable data 跟 shouldComponentUpdate](https://blog.techbridge.cc/2018/01/05/react-render-optimization/)