# [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 &nbsp; &nbsp; |- **App.js** &nbsp; &nbsp; |- TodoItem.js &nbsp; &nbsp; |- **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 &nbsp; &nbsp; |- **App.js** &nbsp; &nbsp; |- TodoItem.js &nbsp; &nbsp; |- useInput.js &nbsp; &nbsp; |- **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 &nbsp; &nbsp; |- App.js &nbsp; &nbsp; |- TodoItem.js &nbsp; &nbsp; |- useInput.js &nbsp; &nbsp; |- 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 學習筆記`