# [FE302] React 基礎:hooks(2)
[TOC]
## [FE302] 2-4 再探 state
### 完成「新增 todo」 的功能
```react=
function App() {
const [todos, setTodos] = React.useState([
123, 456, 555
])
const [value, setValue] = React.useState('')
function handleButtonClick(e) {
setTodos([value, ...todos]) <!-- 修改 todos state by setTodos -->
setValue('') <!-- 書完之後要清空-->
}
function handleInputChange(e) {
setValue(e.target.value) <!-- 改變 state,a=>ab=> abc -->
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<!--onChange-->
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem content={todo} />)
}
</div>
)
}
export default App;
```
也可以使用原本的 DOM 來取出輸入框的內容:
```
const handlBuutonClick = () => {
document.querySelector('.input-todo').value
setTodos([Math.random(), ...todos])
}
<!-- ... -->
<input className="input-todo" type="text" placeholder="todo" />
```
### uncontrolled state: `useRef`
```react=
import {useState, useRef} from 'react'
function App() {
const [todos, setTodos] = React.useState([
123, 456, 555
])
const [value, setValue] = React.useState('')
+ const inputRef = useRef();
function handleButtonClick(e) {
setTodos([value, ...todos])
setValue('')
}
function handleInputChange(e) {
console.log(inputRef)
setValue(e.target.value)
}
const titleSize = 'M';
return (
<div className="App">
+ <input ref={inputRef} type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem content={todo} />)
}
</div>
)
}
export default App;
```
會回傳一個 object,裡面有 `current`。
![](https://i.imgur.com/oY8ekQS.png)
* `inputRef.current` 就是 DOM 元素
```react=
function handleInputChange(e) {
+ console.log(inputRef.current)
setValue(e.target.value)
}
```
![](https://i.imgur.com/danbIn3.png)
* `inputRef.current.value` 就是輸入的內容:
```react=
function handleInputChange(e) {
+ console.log(inputRef.current.value)
setValue(e.target.value)
}
```
![](https://i.imgur.com/804BgSe.png)
:::info
summary
uncontrolled 的意思就是沒有把 input 放到 state;相反的,controlled 的意思就是有把 input 放到 state 裡。
keywords:
function component
controlled component hooks
:::
### 「刪除」、「編輯」的功能、儲存 todo 狀態(未完成已完成)時,需要一個 id 會比較方便
```react=
+ let id = 2; // 因為每次 state 改變,都會重新 call App(),所以要把 id 放在外面。
function App() {
+ // let id = 2; 不能放在裡面,因為每一次 render 都是重新呼叫 App(),為了讓他順利往下數,所以要宣告在外面
const [todos, setTodos] = React.useState([
+ { id: 1, content: 'abc' } // id 自己會持續增加
])
const [value, setValue] = React.useState('')
function handleButtonClick(e) {
setTodos([
+ {
+ id,
+ content: value
+ },
+ ...todos
+ ])
setValue('')
+ id++
}
function handleInputChange(e) {
setValue(e.target.value)
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
+ todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
)
}
export default App;
```
那可以使用 `useState` 表示 `id`嗎?
```react=
let id = 2;
function App() {
// let id = 2;
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc' }
])
const [value, setValue] = React.useState('')
+ const [id, setId] = React.useState(2)
function handleButtonClick(e) {
setTodos([
{
id,
content: value
},
...todos
])
setValue('')
+ setId(id+1)
}
function handleInputChange(e) {
setValue(e.target.value)
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} content={todo.content} />)
}
</div>
)
}
export default App;
```
`id` 可以使用 `useState`,但是 `id` 不會在畫面上顯現,但是 `useState` 只要是 state 有改變,就會 rerender 畫面,所以我們就不會希望 `id` 使用 `state`。
但是 `id` 可以使用 `useRef`,`useRef`
* App.js
```react=
import React from 'react';
import './App.css'; // 因為 webpack 所以可以直接 import css
import styled from 'styled-components';
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG } from './constants/style'
import TodoItem from './TodoItem'
import { useState, useRef } from 'react';
let id = 2; // 因為每次 state 改變,都會重新 call App(),所以要把 id 放在外面。
function App() {
// let id = 2; 不能放在裡面,因為每一次 render 都是重新呼叫 App(),為了讓他順利往下數,所以要宣告在外面
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc' } // id 自己會持續增加
])
const [value, setValue] = React.useState('')
+ const id = useRef(2)
+ // Ref 為了讓值可以保存住, Ref 有點像是可以當 state 也可以直接操作,
+ // 然後在 component rerender 時也不會變,會維持原本的東西。
+ // 使用 useRef 會回傳一個物件:{ ..., current: 2 ,...}
+ // 所以使用 useRef 時,要取`id.current`。
+ // 有 current 的是因為之前的物件指向 parse.reference 等等,所以才要這樣寫。
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
+ id.current++
}
function handleInputChange(e) {
setValue(e.target.value)
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
+ todos.map(todo => <TodoItem key={todo.id} todo={todo} />)
- todos.map(todo => <TodoItem key={id} content={content} />)
}
</div>
)
}
export default App;
```
* todoItem.js
把傳入改成 `todo` object,
```
todo = {
id: 1,
content: 'abas'
}
```
```react=
- export default function TodoItem({ className, size, content }) {
+ export default function TodoItem({ className, size, todo }) {
return (
- <TodoItemWrapper className={className}>
+ <TodoItemWrapper className={className} todo-data-id={todo.id}>
- <TodoContent size={size}>{content}</TodoContent>
+ <TodoContent size={size}>{todo.content}</TodoContent>
<todoButtonWrapper>
<Button>編輯</Button>
<RedButton>刪除</RedButton>
</todoButtonWrapper>
</TodoItemWrapper>
)
}
```
但是沒有看到 `todo-data-id={todo.id}` 在 attribute 上:
![](https://i.imgur.com/yRopj6c.png)
所以改另一個方法:
```
return (
- <TodoItemWrapper className={className} todo-data-id={todo.id}>
+ <TodoItemWrapper className={className}>
+ <TodoContent size={size}>{todo.content}</TodoContent>
- <TodoContent size={size}>{todo.content}, {todo.id}</TodoContent>
```
![](https://i.imgur.com/OiygzLG.png)
就可以看到 `id` 有了!
## [FE302] 2-5 刪除 todo 功能
:::info
App.js:修改 state
todoItem.js:刪除按鈕
有層級關系(父子),App.js 是父關系(parent),todoItem.js 是子關系(chlildren)。
:::
### Q:按了刪除(@ todoItem),如何修改 state(@ App.js)?
1. 在 `App.js` 新增一個 `handleDeleteTodo` function,然後把這個 function 當作 `props` 傳給 `<todoItem>` 。
2. 此時,`todoItem` 的 component 就可以接收一個 `handleDeleteTodo` 這個 function
3. 在按刪除按鈕時,就可以寫 `onClick`,裡面傳入一個 function:`onClick={() => {handleDeleteTodo(todo.id)}}` (call `handleDeleteTodo()` 的 function,並傳入 `todo.id`),等同於呼叫 `App.js` 的 `handleDeleteTodo` function,並傳入 `id`。
4. 重整一次父子關系:把要處理事情的 function 寫在 parent,然後寫在 `<todoItem>` 上,傳給 `Child`,然後 `Child` 再呼叫此 `function`。
5. 處理 `handleDeleteTodo` function
(1) 不能使用 `todos.slice()`,因為會改到 `todos` 內容。
(2) 所以不能使用會修改到 `todos` 內容,得用「產生一新陣列」的方法,就是 array method 的 `array.filter()`
- 示範:`[1,2,3].filter(i => i> 1)`
- 同理:`todos.filter(todo => todo !== todo.id)`
```react=
const handleDeleteTodo = id => { // 傳入的是想要刪除的 ID
setTodos(todos.filter(todo => todo.id !== id))
} // filter 篩 != id 的項目,就是把選到的 id 篩掉,這樣就達到「刪除」的效果。
```
![](https://i.imgur.com/220dsqU.gif)
* todoItem.js
```react=
+ export default function TodoItem({ className, size, todo, handleDeleteTodo }) {
return (
<TodoItemWrapper className={className} todo-data-id={todo.id}>
<TodoContent size={size}>{todo.content}</TodoContent>
<todoButtonWrapper>
<Button>編輯</Button>
+ <RedButton onClick={() => { handleDeleteTodo(todo.id) }}>刪除</RedButton>
</todoButtonWrapper>
</TodoItemWrapper>
)
}
```
* App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef } from 'react';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
let id = 2; // 因為每次 state 改變,都會重新 call App(),所以要把 id 放在外面。
function App() {
// let id = 2; 不能放在裡面,因為每一次 render 都是重新呼叫 App(),為了讓他順利往下數,所以要宣告在外面
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc' } // id 自己會持續增加
])
const [value, setValue] = React.useState('');
const id = useRef(2);
// Ref 為了讓值可以保存住, Ref 有點像是可以當 state 也可以直接操作,
// 然後在 component rerender 時也不會變,會維持原本的東西。
// 使用 useRef 會回傳一個物件:{ ..., current: 2 ,...}
// 所以使用 useRef 時,要取`id.current`。
// 有 current 的是因為之前的物件指向 parse.reference 等等,所以才要這樣寫。
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleInputChange = function (e) {
setValue(e.target.value)
}
+ const handleDeleteTodo = id => {
+ setTodos(todos.filter(todo => todo.id !== id))
+ }
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
+ todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
)
}
export default App;
```
### 加上「編輯」 todo 的功能
#### 修改 isDone: true / false
現在要來修改後的結果是:
![](https://i.imgur.com/kZRXrkK.png)
1. 先設計兩個 sample,一個是已完成`{isDone: true}`;一個是未完成`{isDone: false}`:
```react=
function App() {
const [todos, setTodos] = React.useState([
- { id: 1, content: 'abc'}
+ { id: 1, content: 'abc', isDone: true },
+ { id: 2, content: 'not done', isDone: false }
])
const [value, setValue] = React.useState('');
+ const id = useRef(3);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleInputChange = function (e) {
setValue(e.target.value)
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
}
</div>
)
}
export default App;
```
2. 再來修改 `render` 畫面:
- 已完成:顯示「未完成按鈕」,並在 ~~待辦清單~~ 劃上刪除線
- 未完成:顯示「已完成按鈕」
* todoItem.js
```react=
const TodoContent = styled.div`
color: ${props => props.theme.colors.red_300};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
<!-- 如果 props.isDone 為 true,短路,回傳後面的 <刪除線> -->
+ ${props => props.isDone && `
+ text-decoration: line-through;
+ `}
`
<!-- ...略 -->
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsDone }) {
const handleToggleClick = () => {
handleToggleIsDone(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} todo-data-id={todo.id}>
<TodoContent isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button>
{todo.isDone ? '未完成' : '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper >
)
}
```
除了「三元運算子」 (`todo.isDone ? '未完成' : '已完成'`),還有另一種寫法「短路」,所以可以替換成:
```react=
<Button>
{/* 如果 todo.isDone = true,會短路,顯示 `&&` 後面的值「未完成」*/}
{todo.isDone && '未完成'}
{/* 如果 !todo.isDone = true(也就是 todo.isDone =false),會短路,顯示 `&&` 後面的值「已完成」*/}
{!todo.isDone && '已完成'}
```
#### 補充:
:::info
今天如果傳進去的 `props`,除了會加到 `style-components` 上,也會加到 `DOM` 上面。
![](https://i.imgur.com/GyQiHfK.png)
- 舉例:DOM
```react=
<!-- 新增:id="abc" -->
<TodoContent id='abc' isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
```
如果傳入的 `id`,不想反應在 DOM 結構上,只想當作 `styled-component` 的話,就可以在 `id` 前面加 `$`,就不會加在 DOM。這是 `transient props` 滿新的用法。
如果只是給 `styled-component` 的屬性,前面都會加個 `$`。
修改如下:
- 原本: `isDone={todo.isDone}`
- 修改成:`$isDone={todo.isDone}`
```react=
<TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
```
:::success
這樣才能區別:
1. 傳給 styled-component:變數前面加入 `$`,如 `$isDone`。
3. 傳給 DOM:
:::
現在確認未完成、已完成的 style 後,要來處理標籤的問題。
#### 來處理標籤
:::success
Summary:固定
- 新增 todo:解構([...todos, new-todo])
- 修改 todo:`.map()`
- 刪除 todo:`.filter()`
雖然可以用 `array.slice()` 複製一個新的陣列再做其他事,但推薦用上述的 array.method() 做。
:::
* App.js
```react=
const handleToggleIsDone = id => {
setTodos(todos.map(todo => {
<!-- 如果不是我要修改的 id ,那我就回傳原本的 todo。 -->
if (todo.id !== id) return todo
<!-- 剩下的就是我要修改的 id:新增陣列,並修改某一項元素 -->
return {
...todo, <!-- todo 原本的內容:如 `id: 1, content: 'abc'` -->
isDone: !todo.isDone <!-- 加上新的 isDone 狀態-->
}
}))
}
```
寫好 `handleToggleIsDone()` 再放入`App.js` `<TodoItem></TodoItem>`,傳給 `todoItem.js` 的 `<TodoItem>`。
```react=52
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
在 `todoItem.js` 的 `TodoItem` 可以接收 `handleToggleIsDone`,然後再放入「已經/未完成」按鈕上的 `onClick={}`(`{}` 為在 JSX 寫 JS 語法):
然後可以另設 `handleToggleClick` 再放入 `onClick`(如下):
* todoItem.js
```react=61
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsDone }) {
+ const handleToggleClick = () => {
+ handleToggleIsDone(todo.id)
+ }
return (
<TodoItemWrapper className={className} todo-data-id={todo.id}>
<TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
+ <Button onClick={handleToggleClick}>
{todo.isDone ? '未完成' : '已完成'}
</Button>
<RedButton onClick={() => handleDeleteTodo(todo.id)}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper >
)
}
```
改了一個可以改另一個 `handleDeleteTodo`:
* todoItem.js
```react=61
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsDone }) {
const handleToggleClick = () => {
handleToggleIsDone(todo.id)
}
+ const handleDeleteClick = () => {
+ handleDeleteTodo(todo.id)
+ }
return (
<TodoItemWrapper className={className} todo-data-id={todo.id}>
<TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isDone ? '未完成' : '已完成'}
</Button>
- <RedButton onClick={() => handleDeleteTodo(todo.id)}>刪除</RedButton>
+ <RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper >
)
}
```
:::success
Summary:
1. 使用 `state` 管理 todos
- state todos 改變,畫面就會跟著改變
- 思考:` todo state 長什麼樣子,我的畫面長什麼樣子?`
- 舉例:
- 1. `isDone` 長什麼樣子?
- 2. `todo.content` 會 render 出來
:::
## 2-6 [FE302] TodoList 中場總結
透過 TodoList 將 React 重要概念都學完。
1. **Component**
在 App.js render `<TodoItem>` component,還在裡面 render 各種不同的 `component`。
:::info
使用 component 的方式,去思考 UI。
:::
2. **props**
就像自訂的 HTML property,可以自己傳自己想要的屬性進去 `<component>`,可以接收到 `props` 進來。
3. **style**
如何使用 `create-react-app` 的解法:`inline-style`、`styled-component` 的寫法
:::info
建議使用 `styled-component` 的寫法寫 CSS。
:::
4. **Event Handler**
如何在 React 處理事件?就是在 `<component>` 裡,加上 `onClick`,就不用再在使用元素,然後再加上事件處理。
- 舉例:
- onMouseDown
- onClick
- onSubmit
- onMouseOver
5. **JSX 的語法**`!important`
- 如果在 React 傳 JavaScript 要使用 `{}` 包住。
- 沒有 `if else` 條件式寫法,只有三元運算子、短路的邏輯寫法
- 當要 render 一系列的待辦清單,會使用 `.map()` 變成陣列,讓 JSX 也可以 render 陣列,但也要提供 `key` 給他。
- 合法的 JSX 語法,只能運用基本語法,不像是自由度很高的`template engine` 。
6. **State**
- 如何使用 `useState` 去放 `state` 的初始值,使用 `setState` 去改變 `state`。
- **`state is immutable`** 的概念,不能去修改原本的內容,所以在修改時,會再新增一個陣列給他。
- 在新增時,會用一個新的陣列
- 在刪除時,使用 `array.filter()`,產生新的陣列。
- 在編輯時,使用 `array.map()`,產生新的陣列。
還有一些沒有提到,例如倒數計時。
兩個重要的 hooks:`useState`、`useEffect`。
## 中場休息時間
### 寫 code 的秘密武器:prettier
[prettier](https://create-react-app.dev/docs/setting-up-your-editor/#formatting-code-automatically)
#### 安裝
```shell=
npm install --save husky lint-staged prettier
yarn add husky lint-staged prettier
```
其中:
> * `husky`: makes it possible to use githooks as if they are npm scripts.
> * `lint-staged` allows us to run scripts on staged files in git. See [this blog post](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8) about lint-staged to learn more about it.
> * `prettier` is the JavaScript formatter we will run before commits.
#### 修改 `package.json`
```javascript=
+ "husky": { // 另一個套件
+ "hooks": { // 在 pre-commit 之前,跑 lint-staged 套件
+ "pre-commit": "lint-staged"
+ }
+ }
```
在 pre-commit 之前,跑 lint-staged 套件,所以也要在 `package.json ` 放入`lint-staged` 套件:
針對符合`"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"`規則的檔案跑 `prettier --write` 指令(意即對欲 commit 的檔案作 `prettier`)
```javascript=
"dependencies": {
// ...
},
+ "lint-staged": {
+ "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
+ "prettier --write"
+ ]
+ },
"scripts": {
```
:::info
那在 commit 前做 `prettier`,要怎麼使用呢?
```bash=
$ git add .
$ git commit -m 'add prettier'
# 就會執行很像 ESLint 的執行檢查
```
:::
### [prettier vscode](https://github.com/prettier/prettier-vscode)
在 vscode 上安裝插件 prettier
* 新增文件資料夾為 workspace
![](https://i.imgur.com/9duMTRy.png)
* 進入 settings
![](https://i.imgur.com/b0U64c4.png)
* [從 GitHub 文件看怎麼設定 vscode](https://github.com/prettier/prettier-vscode#visual-studio-code-settings)
![](https://i.imgur.com/YKX8Bh4.png)
勾選 `format on save`
![](https://i.imgur.com/WiCvHy3.png)
### JSX 自動 escape
前端框架,大都會直接幫我們 escape 掉
如果想要直接傳入有 HTML Tag(尚未被 escape)的句子,可以這樣寫:
```react=
<TodoItem dangerouslySetInnerHTML={__html:todo.content}
```
很難記,是故意不讓你記下來,一般也不會特別使用。
但有一個需要注意:`click based JSX`
```
<TodoItem>
<a href={todo.content}>click me</a>
</TodoItem>
```
如果在 anchor 上的 href 放上使用者輸入的話,需要非常注意,像是:
在輸入框上(todo.content)寫`javascript:alert(1)`,
![](https://i.imgur.com/qhcfb5E.png)
點擊 `click me` 就會出現 `alert(1)` 的 JS 語法。
![](https://i.imgur.com/05MMq7H.png)
#### 防範方式
1. 不要這樣使用
2. 由 `href={todo.content}` 改成 `href={window.encodeURIComponent(todo.content)}`
:::info
React 會針對 `<`、`"`、`>` 等等做轉換,這樣就可以防止XSS 攻擊,但是針對 `javascript:alert()` 之類的。
:::
## 第二個 hook:初探 useEffect
### `useEffect` 簡介
`useEffect` 有別於 `useState` ,是傳 function 進去。
### 為什麼要有 `useEffect` ?
`component` 只是一個 function,每次 render 時,都是重新執行一次 `App()` 這個 function ,再 return 內容。
可是 render 完,想要做一些事情,要寫在哪邊?
舉例:假設今天 todo.app 的資料,初始值是從 API 來的,所以要做 API code,但是現在在 App() 沒有地方寫,假設寫在:
```javascript=
function App() {
fetch(...).then()
}
```
這樣每次重新 render 時,都會再發一次 request,而不是我們期望: render 第一次畫面後,才發 request。
所以目前的在這個 function component 沒有地方可以 call request,所以 React 提供了一個 hook,可以做這件事,可以在 `useEffect` 傳一個 function。
```
+ import { useState, useRef, useEffect } from 'react';
- import { useState, useRef } from 'react';
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc', isDone: true },
{ id: 2, content: 'not done', isDone: false }
])
+ useEffect(() => {
+ alert('執行完畢')
+ })
const [value, setValue] = React.useState('');
const id = useRef(3);
```
只要一改變 state 就會出現 `alert()`。每一次 react 執行完 render,都會執行這個 function `{alert('執行完畢')}`
![](https://i.imgur.com/ejhjSJt.png)
也可以試試看把 `{alert('執行完畢')}` 改成 `console.log('after render')`。
### 把 `todos` 存到 `localstorage` 內
:::info
為什麼要存到 `localstorage` 內呢?
每次只要重新整理,剛剛填寫的 `todo` 就會清空,把 `todos` 存進去 `localstorage` 這樣一來,每次重新整理 `todo` 就不會消失。
:::
#### 先寫一個存入 `localstorage` 的 function
#### 測試一下剛剛把 new todo 寫入 `setTodos`,會不會直接印:
![](https://i.imgur.com/J21dIAx.png)
```javascript=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect } from 'react';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
let id = 2;
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc', isDone: true },
{ id: 2, content: 'not done', isDone: false }
])
// useEffect(() => {
// alert('執行完畢')
// })
const [value, setValue] = React.useState('');
const id = useRef(3);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
console.log(todos)
setValue('')
+ window.localStorage.setItem('todos', [
+ {
+ id: id.current,
+ content: value,
+ },
+ ...todos
+ ])
id.current++
}
```
把 line 38 ~ line 44 改成:
```javascript=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect } from 'react';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
+ function writeTodosToLocalStorage(todos) {
+ window.localStorage.setItem('todos', JSON.stringify(todos))
+ }
```
測試看看剛新增的 todo,會不會顯示在 console.log 上:
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect } from 'react';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
function writeTodosToLocalStorage
let id = 2;
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc', isDone: true },
{ id: 2, content: 'not done', isDone: false }
])
// useEffect(() => {
// alert('執行完畢')
// })
const [value, setValue] = React.useState('');
const id = useRef(3);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
+ console.log(todos)
setValue('')
id.current++
}
```
新增第三個,但是 console.log 只有兩個 todo,不會即時更新:
![](https://i.imgur.com/MIdbzRg.png)
這個是「非同步」,非「同步」,所以呢就要把新增的放在 function 輸入參數的地方:
```javascript=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect } from 'react';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
let id = 2;
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc', isDone: true },
{ id: 2, content: 'not done', isDone: false }
])
// useEffect(() => {
// alert('執行完畢')
// })
const [value, setValue] = React.useState('');
const id = useRef(3);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
+ writeTodosToLocalStorage([
+ {
+ id: id.current,
+ content: value
+ },
+ ...todos
+ ])
setValue('')
id.current++
}
const handleInputChange = function (e) {
setValue(e.target.value)
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
setTodos(todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
}))
+ writeTodosToLocalStorage(
+ todos.map(todo => {
+ if (todo.id !== id) return todo
+ return {
+ ...todo,
+ isDone: !todo.isDone
+ }
+ })
+ )
}
```
也可以另外新增 `newTodo` 來放 todo,像是:
```javascript=
const handleToggleIsDone = id => {
+ const newTodo = () => {
+ return todos.map(todo => {
+ if (todo.id !== id) return todo
+ return {
+ ...todo,
+ isDone: !todo.isDone
+ }
+ })
+ }
+ setTodos(newTodo())
}
```
因為在新增 todo 時,不是「同步」,是「非同步」,所以當 todos 的 state 改變,沒辦法直接存到 local.storage,也要寫一個 [todo, ...todo] 的新 array 存到 local.storage。這樣一來的話,在新增、isDone、刪除的 todo 改變,都要存一次 local.storage,現在這種作法,就是以前使用 jQuery 的想法,在變動時,去存到 local.storage。另外,這幾個功能,如 `setTodos(新增todo)`、`setTodos(狀態變成未完成)` 存 localstorage 的共通點都是:`todo 的 state` 改變,剛剛學到的 `useEFfect` 會在每次 render 完執行 `useEFfect`,然後每次 render 表示我們的 state 有改變,所以第一種方法就是這樣寫:
* 每一次 render 完,我都把最新的 todo 寫到 localstorage 內。
```react=
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log(JSON.stringify(todos))
})
```
![](https://i.imgur.com/G3GTsjY.png)
只要有修改 state,就會改變 state,重新 render,像是輸入 `a`、`b` 就會重新 render 一次,有點沒有效率,但現在只要有改變 todo (例如:新增 newTodo),都會寫到 local storage 裡面,這樣一來,local storage 和 state 的資料就會一樣、資料同步。
但我們現在只想要在「todo 改變」的時候,而不是每次在輸入框輸入`a`、`b` 還沒有新增 todo 時,就重新 render,**`只想要在「todo 改變」的時候`**,此時 `react hook, useEffect` 有提供第二個參數(為陣列),就可以放我們想要關注的資料,例如 `todos`,意思就是說:當我們想要**改變 todo 時,我要執行這個 `useEffect`**,除了第一次 render,只要是 `todos` 有改變,就會重新執行這個 `useEffect`。
```react=
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log(JSON.stringify(todos))
+ }, [todos])
```
![](https://i.imgur.com/6pgJ7Wv.png)
![](https://i.imgur.com/C3m2UhU.png)
可能會想在頁面重新整理完,把 local storage todo 撈出來,放在 todos 裡面:
```
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log(JSON.stringify(todos))
}, [])
```
當我這個陣列為 `[]`,就執行這個 `useEffect`,但因為他是 `[]`是空的,所以裡面的東西(陣列內的東西不會變,不是陣列)不會變,所以只有第一次 render 會執行這個 useEffect,其他例如:修改 todos,都不會改變,所以就可以拿來做初始化的資料,例如取 API,舉例:
```react=
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
//...
+ useEffect(() => {
+ const todoData = window.localStorage.getItem('todos') || ""
+ if (todoData) { // 裡面有資料才 setTodos
+ setTodos(JSON.parse(todoData)) // 把 todoData 放回 state 裡面。
+ }
+ }, [])
// ...
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
+ writeTodosToLocalStorage(newTodo())
}
```
![](https://i.imgur.com/xC9jLZr.png)
重新整理後,東西不會變。
:::info
![](https://i.imgur.com/JvH4iEZ.png)
id 相同的問題
後來是先用 `localStorage.clear():清除所有localStorage的資料`,但還是無法解決根本上的問題。
:::
## 初探 useLayoutEffect & lazy initiallizer
![](https://raw.githubusercontent.com/donavon/hook-flow/master/hook-flow.png)
:::info
初探 `useLayoutEffect`
「render 完,**瀏覽器 paint 以前**,你想做什麼?」
在瀏覽器 paint 之前,把資料塞進去,就ˇ不會看到重新整理後瞬間空白的頁面了。
初探 `useEffect`
「render 完,**瀏覽器 paint 以後**,你想做什麼?」
:::
在重新整理後,會有閃一下出現白色頁面,然後才 render 出 todolist,那要怎麼解決閃一下白色頁面的問題呢?
過程:
1. 初始資料
```react=
function App() {
const [todos, setTodos] = React.useState([
{ id: 1, content: 'abc', isDone: true },
{ id: 2, content: 'not done', isDone: false }
])
...
```
2. 將改變的 todo 存進 local storage
```react=
useEffect(() => {
const todoData = window.localStorage.getItem('todos') || "";
if (todoData) {
setTodos(JSON.parse(todoData));
}
}, []);
```
3. 重新 render 時,會先 run 1. 再 2.才會更新我的資料,再 rerender 一次,這之間就有一個空白頁面閃一下。
4. **那要怎避開閃一下的問題呢?**
- `useLayoutEffect`
`render 完,瀏覽器 paint 以前,你想做什麼?` 在瀏覽器 paint 以前,就會執行 `useLayoutEffect`。
```react=
- import { useState, useRef, useEffect} from 'react';
+ import { useState, useRef, useEffect, useLayoutEffect } from 'react';
//...
- useEffect(() => {
+ useLayoutEffect(() => {
const todoData = window.localStorage.getItem('todos') || "";
if (todoData) {
setTodos(JSON.parse(todoData));
}
}, []);
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
```
`render 完,瀏覽器 paint 以前,你想做什麼?` 在瀏覽器 paint 以前,就會執行 `useLayoutEffect`,這樣就解決了「空白頁一閃」的問題。
:::danger
很重要!!記起來!
**mount:把 componenent 放到畫面上。**
**update:更新 state 時。**
React Hook Flow Diagrame 流程:
1. render
1. React Update DOM
1. Run layoutEffect
今天在 browser 畫畫面之前,就提早去改 state,提早畫面更新 DOM,就會顯示最新的那一次結果,而不是第一次 render 的結果。
3. Browser Paints screen
4. Run Effects
![](https://i.imgur.com/xUMCYAr.png)
:::
```react=
function App() {
- const [todos, setTodos] = React.useState([
- { id: 1, content: 'abc', isDone: true },
- { id: 2, content: 'not done', isDone: false }
- ])
// 每一次
+ const todoData = window.localStorage.getItem('todos') || ""; // 每一次 render 時,都還有在 run
+ const [todos, setTodos] = React.useState(JSON.parse(todoData) || []) // 把初始 state 值改掉。
// 放在 useState 的值,只有第一次有效。
// 在 local storage 改東西,
const [value, setValue] = React.useState('');
const id = useRef(3);
- const todoData = window.localStorage.getItem('todos') || "";
- if (todoData) {
- setTodos(JSON.parse(todoData));
- }
- }, []);
```
![](https://i.imgur.com/ERXbRye.png)
:::info
基本結構用法:`const [state, setState] = useState(initialState)`
1. state 為我們要設置的狀態。
1. setState 為更新 state 的方法,命名依照專案的需求而定。
1. initialState 為初始的 state,可以是任意的資料型別,也可以是 callback Function,但要注意的是,最後要 return 回一個值。
取自 [React Hooks 學習筆記 useState、useEffect](https://medium.com/vita-for-one/react-hooks-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-usestate-useeffect-usecontext-b11c33e69bea)
另外用法:`const [state, setState] = useState(callback)`
可以傳一個 function,function return 的值,就是我們的初始值,而這個 function 只會執行一次,所以可以改成以下這樣寫:
:::
每一次重新 render 時,都會存一次 `window.localstorage`,然後 usestate 只會在第一次 render 時執行,這樣每次重新整理,就要重新執行一次 `window.localstorage` 跟 ` usestate`,因為每次 render 時(例如第二次 render),`window.localstorage` 還要把 todoData 取出,然後 `useEffect`還要再放一次 todoData,但因為我們有初始值了,所以 react 會忽略初始值,很沒有效率,
`const [state, setState] = useState(callback)`
可以傳一個 function,function return 的值,就是我們的初始值,而這個 function 只會執行一次,所以可以改成以下這樣寫(使用 `console.log(init)` 來測試一下是否只有執行一次:
```react=
function App() {
- const todoData = window.localStorage.getItem('todos') || ""; // 每一次 render 時,都還有在 run
- const [todos, setTodos] = React.useState(JSON.parse(todoData) || []) // 把初始 state 值改掉。
+ const [todos, setTodos] = React.useState(() => {
+ console.log('init') // 測試一下是否只有第一次 render會出現。
+ const todoData = window.localStorage.getItem('todos') || ""; // 每一次 render 時,都還有在 run
+ return JSON.parse(todoData) || []
+ }) // 把初始 state 值改掉。
const [value, setValue] = React.useState('');
const id = useRef(3);
```
![](https://i.imgur.com/e0HCk9a.png)
發現一個錯誤: todos 的 id 重複:
```react=
function App() {
+ const id = useRef(1);
const [todos, setTodos] = React.useState(() => {
console.log('init')
const todoData = window.localStorage.getItem('todos') || ""; // 每一次 render 時,都還有在 run
+ if (todoData) { // 有資料就可以把 id
+ todoData = JSON.parse(todoData);
+ id.current = todoData[0].id +1
+ } else {
+ todoData = []
+ }
+ return JSON.parse(todoData) || []
+ })
const [value, setValue] = React.useState('');
- const id = useRef(3);
```
記得要把 localstorage 清掉:
![](https://i.imgur.com/ooMidto.png)
只有第一次 render 出現 `console.log('init')`
![](https://i.imgur.com/mSmg81r.png)
:::info
`const [todos, setTodos] = useState(() => {...})` 其中,`useState(()=>{})` 傳的東西,就稱為 ` run lazy initializer`,只有第一次才做這件事,做完才 render。
如果沒有那麼複雜的話,就直接寫在 `userState(1)` 就可以了。
```react=
const [todos, setTodos] = useState(() => {
console.log('init')
let todoData = window.localStorage.getItem('todos') || "";
if (todoData) { // 有資料就可以把 id
todoData = JSON.parse(todoData);
id.current = todoData[0].id + 1;
} else {
todoData = [];
}
return todoData;
})
```
:::
Review:
先前如果把第一次 render 前想要顯示在網頁上的初始資料放在 `useState(()=>{})` 外面跟前面 code,每次 state 改變就重新 render,重新執行一次初始資料,然後再放在 `useState(初始資料)`,會沒有效率。
:::info
### **初探 `useLayoutEffect`**
「render 完,**瀏覽器 paint 以前**,你想做什麼?」
```react=
useLayoutEffect 改變 state,那 state 換了 render 之後,可以想成第一次跟第二次的 render 綁在一起,只會看到第二次的結果。所以在 browser paint 之前會做一些事情。
```
### 初探 `useEffect`
「render 完,**瀏覽器 paint 以後**,你想做什麼?」
```react
可以用 useEffect(()=>{ return 我想做的事情}),做一些比較複雜的事情只要做一次就可以,不用每次都做。
```
### cf
```
最重要還是這兩個觀念,具有些微的差別,有九成情形只會使用 `useEffect`。
```
### `useEffect` 使用方式複習:
1. `useEffect(()=>{})` 每次 render 都會執行。
1. `useEffect(()=>{}, [])` 第一次 render 會執行,等於 `mount`(把東西放到 DOM 上) 之後執行。
1. `useEffect(()=>{}, [todos])` 當 `[todos]` state 改變時,才會重新執行。
:::
## 再探 `useEffect`
### `useEffect` 閱讀推薦
1. [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/)
2.[How Are Function Components Different from Classes?](https://overreacted.io/how-are-function-components-different-from-classes/)
### hook flow diagram (hook 生命週期)
回顧:
```react=
useEffect(() => {
writeTodosToLocalStorage(todos);
}, [todos]);
```
當 `[todos]` 的 state 改變,才會去執行 `() => {
writeTodosToLocalStorage(todos);
}` 的 code。
但這個 `useEffect(()=>{ return ()=>{...} }` 中,可以 return 另一個 function,如下:
```
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log('useEffect: todos', JSON.stringify(todos)) //
// clean up function
return () => {
console.log('clearEffect: todos', JSON.stringify(todos)) // 上一次 render 的 todos
}
}, [todos]);
```
![](https://i.imgur.com/pyAhAz1.png)
```react=
第一次 render:
第一次執行時的 App() {
todos: todos [{"id":1,"content":"ㄇ"}]
}
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log('useEffect: todos', JSON.stringify(todos))
// clean up function
return () => {
console.log('clearEffect: todos', JSON.stringify(todos))
}
}, [todos]);
第二次 render,App() {
1. 清掉 Effect:(return 內的函式)
- useEffect todos: todos [{"id":1,"content":"ㄇ","isDone":true}]
2. 執行下一個 effect(執行最新的 todos)
// 有類似 closure 的概念
```
1. 每次 render 後的 `[todos, setTodos]` 都不一樣:
- 第一次 render:(`不會改變,永遠長這個樣子`)
- todos: `todos [{"id":1,"content":"ㄇ"}]`
- setTodos:調整下一次 render 的 todos 長怎樣,不會去改變第一次 render 的 todos 內容。
- 所以第一次 render 的 todos 永遠都是`todos [{"id":1,"content":"ㄇ"}]` 不會改變。
- 第二次 render:
- todos: `[{"id":1,"content":"ㄇ","isDone":true}]`
- 每次 render 都是不一樣的 function call,每次 render 都會有自己的 `todos`、`setTodos`,第一次 render 的 todos 與第二次 render 的 todos 不一樣。
### 什麼時候會用到 `useEffect(()=> { ... return (何時會用到這個呢?)=>{}})
以官網為舉例,UserId 會去監聽,連到某個 UserId,去拿資料,做處理。
當我們今天換了 UserId,要先把 UserId 斷掉,才能連接下一個,這就是為什麼需要 useEffect cleanUp function,因為需要做到一些去清掉的事情,這就是 cleanup function 存在意義。
```react=
useEffect(()=> {
WebSocket.CONNECTING(UserId).subscribe(()=>{
// ...
})
return () => {
WebSocket.disconnect(UserId)
}
}, [UserId])
```
另外回顧:
只有第一次 render 會執行:
```
useEffect(()=>{
}, [])
```
![](https://i.imgur.com/ZMHIEut.png)
當我們清掉 Effect時,除在上述講的 `update`,還有另外一種情形:`Unmount`:
今天想在** component 不見(unmount)**時,想執行事件時,可以這樣寫,可以確保只有在 unmount 才會執行:
```react=
useEffect(()=> {
return () => {
console.log('unmount')
}
},[])
```
:::info
執行 cleanUp 時機點有二:
1. 要執行下一個 effect 時,要先把上一個 effect 清掉
2. component unmount 時,會把 effect 清掉。
:::
先前提到,要在陣列裡面的東西有改變,才會再來執行一次,但是陣列是空的,陣列內的東西不會改變,所以這個 useEffect 只會執行一次,那所以就沒有執行第二次,所以這裡面的東西(`return () => {
console.log('unmount')
}`)就不會因為執行新的 effect 被清掉,所以只會在 component unmount 時被清掉。
所以當 componenent 被 unmount 時,在這(`return () => {
console.log('unmount')`)去做一些事情,例如頁面想清掉什麼東西的時候,就可以寫在這邊。
:::info
總結:
```react=
useEffect(() => {
console.log('mount')
// clean up function
return () => {
console.log('unmount')
}
}, [todos]);
```
:::
## hooks 重要觀念
hooks 不能被 `if else`、`foreach` 包起來。
```
if (true) {
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log('useEffect: todos', JSON.stringify(todos))
// clean up function
return () => {
console.log('clearEffect: todos', JSON.stringify(todos))
}
}, [todos]);
}
```
錯誤訊息:不能把 `react hook: useEffect` 包在裡面
```
Line 38:5: React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
Search for the keywords to learn more about each error
```
只能在 `useEffect` 裡面寫判斷式,像是這樣:
```react=
useEffect(() => {
if (!todos) {
writeTodosToLocalStorage(todos);
console.log('useEffect: todos', JSON.stringify(todos))
}
// clean up function
return () => {
console.log('clearEffect: todos', JSON.stringify(todos))
}
}, [todos]);
```
## 寫一個自己的 hook → 完整把 UI 及功能分開
### `Value` 實作
只要再輸入值時,都會有 value & setValue,可以把他包成一個 hook。
:::info
|- index.js
|- src
|- **App.js**
|- TodoItem.js
|- **useInput.js** (new file)
:::
* 新增一個檔案 @ src/useInput.js
```react=
import { useState } from 'react';
export default function useInput() {
const [value, setValue] = useState("");
const handleInputChange = e => {
setValue(e.target.value)
}
return {
value,
setValue,
handleInputChange
}
}
```
* src/App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
+ import useInput from './useInput';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
let id = 0;
function App() {
const id = useRef(1);
const [todos, setTodos] = useState(() => {
console.log('init')
let todoData = window.localStorage.getItem('todos') || "";
if (todoData) { // 有資料就可以把 id
todoData = JSON.parse(todoData);
id.current = todoData[0].id + 1;
} else {
todoData = [];
}
return todoData;
})
- const [value, setValue] = React.useState('');
+ const { value, setValue, handleInputChange } = useInput();
useEffect(() => {
writeTodosToLocalStorage(todos);
console.log('useEffect: todos', JSON.stringify(todos))
// clean up function
return () => {
console.log('clearEffect: todos', JSON.stringify(todos))
}
}, [todos]);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
+ const handleInputChange = function (e) {
+ setValue(e.target.value)
+ }
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
writeTodosToLocalStorage(newTodo())
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
+- <button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
### 改成自製的 hook 好處:有第二個 input,可以比照辦理
可以把共同的邏輯抽出來做成自製 hook!
```react=
// const { value, setValue, handleInputChange } = useInput();
// 比照辦理,使用 ES6 語法:幫他取另一個名字。
const { value: todoName, setValue:setTodoName, handleInputChange:handleTodoName } = useInput();
```
### 把 todo 抽出,做成共通的邏輯
目標:
```
const [todos, setTodos] = useTodos(); // 自製 hook
```
:::warning
hook 就是一個 function,
constant hook 檔案一定要 `usexxxx.js` 的 `use` 開頭。
:::
:::info
|- index.js
|- src
|- **App.js**
|- TodoItem.js
|- useInput.js
|- **useTodo.js** (new file)
:::
* src/App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import useInput from './useInput';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
let id = 0;
function App() {
const id = useRef(1);
+ const [todos, setTodos] = useTodos();
- const [todos, setTodos] = useState(() => {
- console.log('init')
- let todoData = window.localStorage.getItem('todos') || "";
- if (todoData) { // 有資料就可以把 id
- todoData = JSON.parse(todoData);
- id.current = todoData[0].id + 1;
- } else {
- todoData = [];
- }
- return todoData;
- })
- const { value, setValue, handleInputChange } = useInput();
- useEffect(() => {
- writeTodosToLocalStorage(todos);
- console.log('useEffect: todos', JSON.stringify(todos))
- // clean up function
- return () => {
- console.log('clearEffect: todos', JSON.stringify(todos))
- }
- }, [todos]);
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
writeTodosToLocalStorage(newTodo())
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
* src/useTodo.js
```react=
+ import { useState, useEffect } from 'react';
+ export default function useInput() {
+ const [todos, setTodos] = useState(() => {
+ let todoData = window.localStorage.getItem('todos') || "";
+ if (todoData) { // 有資料就可以把 id
+ todoData = JSON.parse(todoData);
+ id.current = todoData[0].id + 1;
+ } else {
+ todoData = [];
+ }
+ return todoData;
+ })
+ useEffect(() => {
+ writeTodosToLocalStorage(todos);
+ // clean up function
+ return () => {
+ }
+ }, [todos]);
+ return {
+ todos,
+ setTodos
+ }
+ }
```
![](https://i.imgur.com/VvcSYbf.png)
* App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import useInput from './useInput';
+ import useTodos from './useTodos';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
let id = 0;
function App() {
const id = useRef(1);
const [todos, setTodos] = useTodos();
+ const [value, setValue, handleInputChange] = useInput();
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
writeTodosToLocalStorage(newTodo())
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
![](https://i.imgur.com/G6Is1io.png)
* useTodos.js
```react=
import { useState, useEffect, useRef } from 'react';
// 把 `JSON.stringify(todos)` 寫入 localstorage
+ function writeTodosToLocalStorage(todos) {
+ window.localStorage.setItem('todos', JSON.stringify(todos))
+ }
export default function useTodos() {
+ const id = useRef(1);
const [todos, setTodos] = useState(() => {
let todoData = window.localStorage.getItem('todos') || "";
if (todoData) { // 有資料就可以把 id
todoData = JSON.parse(todoData);
id.current = todoData[0].id + 1;
} else {
todoData = [];
}
return todoData;
})
useEffect(() => {
writeTodosToLocalStorage(todos);
// clean up function
return () => {
}
}, [todos]);
return {
todos,
setTodos,
+ id
}
}
```
* App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import useInput from './useInput';
import useTodos from './useTodos';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
function App() {
- const id = useRef(1);
- const [ todos, setTodos ] = useTodos();
+ const { id, todos, setTodos } = useTodos();
+ const {value, setValue, handleInputChange} = useInput();
- const [value, setValue, handleInputChange] = useInput();
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
- writeTodosToLocalStorage(newTodo())
}
const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
### 把功能全拆到自製 hook,把 UI 及功能分開
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
- import useInput from './useInput';
import useTodos from './useTodos';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
function App() {
- const { id, todos, setTodos } = useTodos();
- const {value, setValue, handleInputChange} = useInput();
+ const {
+ id,
+ todos,
+ setTodos,
+ handleButtonClick,
+ handleDeleteTodo,
+ handleToggleIsDone,
+ value,
+ setValue,
+ handleInputChange } = useTodos();
- function handleButtonClick(e) {
- setTodos([
- {
- id: id.current,
- content: value
- },
- ...todos
- ])
- setValue('')
- id.current++
- }
- const handleDeleteTodo = id => {
- setTodos(todos.filter(todo => todo.id !== id))
- }
-
- const handleToggleIsDone = id => {
- const newTodo = () => {
- return todos.map(todo => {
- if (todo.id !== id) return todo
- return {
- ...todo,
- isDone: !todo.isDone
- }
- })
- }
- setTodos(newTodo())
- }
- const titleSize = 'M';
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
* src/useTodos.js
```react=
import { useState, useEffect, useRef } from 'react';
+ import useInput from './useInput';
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
export default function useTodos() {
const id = useRef(1);
+ const {value, setValue, handleInputChange} = useInput();
const [todos, setTodos] = useState(() => {
let todoData = window.localStorage.getItem('todos') || "";
if (todoData) { // 有資料就可以把 id
todoData = JSON.parse(todoData);
id.current = todoData[0].id + 1;
} else {
todoData = [];
}
return todoData;
})
+ function handleButtonClick(e) {
+ setTodos([
+ {
+ id: id.current,
+ content: value
+ },
+ ...todos
+ ])
+ setValue('')
+ id.current++
+ }
+ const handleDeleteTodo = id => {
+ setTodos(todos.filter(todo => todo.id !== id))
+ }
+ const handleToggleIsDone = id => {
+ const newTodo = () => {
+ return todos.map(todo => {
+ if (todo.id !== id) return todo
+ return {
+ ...todo,
+ isDone: !todo.isDone
+ }
+ })
+ }
+ setTodos(newTodo())
+ }
useEffect(() => {
writeTodosToLocalStorage(todos);
// clean up function
return () => {
}
}, [todos]);
return {
todos,
setTodos,
id,
+ handleButtonClick,
+ handleDeleteTodo,
+ handleToggleIsDone,
+ value,
+ setValue,
+ handleInputChange
}
}
```
把邏輯抽到 hooks 內, App.js 只剩下 UI,UI介面和邏輯完全分開,非常乾淨的介面,如果別人想要寫 todos,可以把 hooks 給他,自己寫介面。
### 最終版本:
:::info
|- index.js
|- src
|- App.js
|- TodoItem.js
|- useInput.js
|- useTodos.js
:::
* index.js
```react=
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals'; // 和網站效能有關
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
red_300: '#e61111',
red_400: '#440000',
red_500: '#660000'
}
}
ReactDOM.render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```
* src/App.js
```react=
import React from 'react';
import styled from 'styled-components';
import TodoItem from './TodoItem'
import { useState, useRef, useEffect, useLayoutEffect } from 'react';
import useTodos from './useTodos';
const GreyTodoItem = styled(TodoItem)`
background: grey;
`
function App() {
const {
id,
todos,
setTodos,
handleButtonClick,
handleDeleteTodo,
handleToggleIsDone,
value,
setValue,
handleInputChange } = useTodos();
// const {value, setValue, handleInputChange} = useInput();
return (
<div className="App">
<input type='text' placeholder='new todo' value={value} onChange={handleInputChange} />
<button onClick={handleButtonClick}>Add todo</button>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} handleToggleIsDone={handleToggleIsDone} />)
}
</div>
)
}
export default App;
```
* src/TodoItem.js
```react=
import React from 'react';
import logo from './logo.svg';
import './App.css'; // 因為 webpack 所以可以直接 import css
import styled from 'styled-components';
import { MEDIA_QUERY_MD, MEDIA_QUERY_LG } from './constants/style'
const TodoItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border: 1px solid black;
& + & {
margin-top: 12px;
}
`
const TodoContent = styled.div`
color: ${props => props.theme.colors.red_300};
font-size: 12px;
${props => props.size === 'XL' && `
font-size: 20px;
`}
${props => props.$isDone && `
text-decoration: line-through;
`}
`
//JS 放 function 一般放 arrow func,一般用 props,
const TodoButtonWrapper = styled.div``
const Button = styled.button`
padding: 4px;
color: black;
font - size: 20px;
${MEDIA_QUERY_MD} {
font - size: 16px;
}
${MEDIA_QUERY_LG} {
font - size: 12px;
}
& + & {
margin- left: 4px;
}
:hover {
color: white;
background: blue;
}
`
const RedButton = styled(Button)`
color: red;
`
// button + button 也就是第二個
export default function TodoItem({ className, size, todo, handleDeleteTodo, handleToggleIsDone }) {
const handleToggleClick = () => {
handleToggleIsDone(todo.id)
}
const handleDeleteClick = () => {
handleDeleteTodo(todo.id)
}
return (
<TodoItemWrapper className={className} todo-data-id={todo.id}>
<TodoContent $isDone={todo.isDone} size={size}>{todo.content}</TodoContent>
<TodoButtonWrapper>
<Button onClick={handleToggleClick}>
{todo.isDone ? '未完成' : '已完成'}
</Button>
<RedButton onClick={handleDeleteClick}>刪除</RedButton>
</TodoButtonWrapper>
</TodoItemWrapper >
)
}
```
* src/useInput.js
```react=
import { useState } from 'react';
export default function useInput() {
const [value, setValue] = useState("");
const handleInputChange = e => {
setValue(e.target.value)
}
return {
value,
setValue,
handleInputChange
}
}
```
* src/useTodos.js
```react=
import { useState, useEffect, useRef } from 'react';
import useInput from './useInput';
// 把 `JSON.stringify(todos)` 寫入 localstorage
function writeTodosToLocalStorage(todos) {
window.localStorage.setItem('todos', JSON.stringify(todos))
}
export default function useTodos() {
const id = useRef(1);
const {value, setValue, handleInputChange} = useInput();
const [todos, setTodos] = useState(() => {
let todoData = window.localStorage.getItem('todos') || "";
if (todoData) { // 有資料就可以把 id
todoData = JSON.parse(todoData);
id.current = todoData[0].id + 1;
} else {
todoData = [];
}
return todoData;
})
function handleButtonClick(e) {
setTodos([
{
id: id.current,
content: value
},
...todos
])
setValue('')
id.current++
}
const handleDeleteTodo = id => {
setTodos(todos.filter(todo => todo.id !== id))
}
const handleToggleIsDone = id => {
const newTodo = () => {
return todos.map(todo => {
if (todo.id !== id) return todo
return {
...todo,
isDone: !todo.isDone
}
})
}
setTodos(newTodo())
}
useEffect(() => {
writeTodosToLocalStorage(todos);
// clean up function
return () => {
}
}, [todos]);
return {
todos,
setTodos,
id,
handleButtonClick,
handleDeleteTodo,
handleToggleIsDone,
value,
setValue,
handleInputChange
}
}
```
## hooks 總結
1. `useState`:讓你的 function component 擁有 state 可以去管理內部的狀態
2. `useEffect` 在 component render 完之後,在 browser paint 之後,要做什麼事情?***
- `非常常用`
- 通常不會把 `localStorage` 直接寫在 `App()` 裡面,因為每次 render 都會再執行一次。
- ~~與其讓 `localStorage` 在每次 render 都做,會影響效能~~,還不如在 render 完之後做。
4. `useLayoutEffect` 在 component render 完之後,在 browser paint 之前,要做什麼事情?
5. [Hooks API 參考](https://zh-hant.reactjs.org/docs/hooks-reference.html)
- React 官方提供的 hook
- hooks 可以使用官方提供、別人製作、自己製作的 hook。
- 像是別人做的 hook, [useLocalStorage](https://usehooks.com/useLocalStorage)
進階:
1. hook 自製
初階:
1. 怎麼使用`useState` 管理狀態
2. 怎麼使用 `useEffect` 做事情。
再次推薦 [從 Hooks 開始,讓你的網頁 React 起來](https://ithelp.ithome.com.tw/users/20103315/ironman/2668?page=1)
###### tags: `MTR05` `Lidemy 學習筆記`