useEffect
通常,想要執行什麼事情時,會寫在兩個地方:
- useEffect 裡面
- event handler 裡面
不會把要做的事情直接寫在 function component 裡面,原因為:
如果直接寫在 function component 裡面,那就是「每次 render 時都會執行」
但是這樣一邊 render 一邊執行要做的事情,會阻礙 render 的進行,效能會比較差
比較好的做法是:等 render 完成之後,再執行要做的事情
render 機制
如果要避免 re-render 的話,就會使用到 useCallback, useMemo, memo
安裝 ESLint extension (增進程式碼品質的第一種方式)

安裝完之後,要把 vs code 右下角的 ESLint 打勾來啟用它

利用 propTypes 驗證 props (增進程式碼品質的第二種方式)
propTypes 是 React 提供的方法,用來檢查 prop 的型態是否正確
如果沒有按照 propTypes 的規則去使用這個 prop 的話,就會在 console 顯示 warning(但不會影響到程式碼的執行)
要使用 propTypes,就要先改一下 ESLint 的規則,步驟如下:
先在根目錄新增一個檔案,叫做 .eslintrc.json,在裡面要寫的內容如下
"extends": ["react-app"] 意思是:先延續 Create React App 的規則
"rules" 意思是:再新增我自己訂的規則,"warn" 就是 warning 的意思
.eslintrc.json:
{
"extends": ["react-app"],
"rules": {
"react/prop-types": "warn"
}
}
加上 "react/prop-types" 這個 rule 之後,就會規定我「每個 component 都一定要寫 prop types」,寫起來會有一點麻煩,但好處是可以幫我避免一些預期之外的錯誤
在 TodoItem.js 使用 propTypes
要使用 propTypes 之前,要先在最上方引入:
PropTypes 是 React 提供的,會幫助我定義一些已經有的類型
import PropTypes from "prop-types";
然後,就可以幫 TodoItem component 定義 propTypes 了(就寫在 TodoItem component 的後面):
後面加上 .isRequired 意思就是:一定要傳這個 prop,否則就會在 console 出現 warning
PropTypes.func 的 func 是 function 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.object,
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
如果想要更詳細的寫出 todo 這個 object 是長什麼樣子,例如 todo 這個 object 裡面會用到 id, content, isFinished 這些 key,就可以用 PropTypes.shape() 來寫:
bool 是 boolean 的簡寫
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired,
todo: PropTypes.shape({
id: PropTypes.number,
content: PropTypes.string,
isFinished: PropTypes.bool,
}),
handleDeleteTodo: PropTypes.func,
handleToggleIsFinished: PropTypes.func,
};
以 state 為中心去思考

如果要把這個 todo list 的畫面變成 json 格式的資料存下來,可以這樣寫:
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
itemLeftCount: 1,
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
以資料為中心去思考,有兩個重點:
重點一:資料改變會需要改變畫面,這個資料就是 state
必備的資料(無法透過其他資料算出來的),才會是 state
資料改變但不影響畫面,它就會用 useRef 來存
當我想要存某個狀態,但這個狀態不影響畫面時,就可以用 useRef 來存
例如:todo list 裡面的 todoId
因為畫面上不需要顯示 id,所以 todoId 不會被放在 state 裡面。當 todoId 遞增時,會用 useRef 來存
const todoId = useRef(0);
要用 todoId 的值時,就要寫 todoId.current 才會是我要的值
會需要用 .current 是因為 JS 的 pass by reference -> 一定要是一個「物件」的屬性,才可以被記下來
重點二:state vs derived state
derive 是「從…中得到」的意思
derived state 就是:不需要直接放在 state 裡面,它是可以被計算出來的 -> 可以透過現有的 state 去組合/計算而成 -> 從現有的 state 衍伸出來的就叫做 derived state
如果是 derived state 就不要放在 state 裡面,否則會很難維護
以上面的 todo list 來說,itemLeftCount 就是 derived state
我根本不需要把 itemLeftCount 放在 state 裡面,我只要去 todos 裡面計算出有多少筆資料的 isChecked 是 false,就會是 itemLeftCount 的結果
// 放在 state 裡面
{
inputValue: 'abc',
todos: [
{
content: 'work out',
isChecked: true
},
{
content: 'buy milk',
isChecked: false
}
],
filter: 'active'
}
// 沒有放在 state 裡面
todoId: 2
App.js:
filteredTodos 就是:選擇不同的 filter 之後要相對應 render 出來的 todos
filteredTodos 是 derived state,因為可以從 filter 和 todos 的狀態來推算出 filteredTodos
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 input 欄位每輸入一個字,在 console 就會印出一次 "render~"

原因為:
在 input 欄位每輸入一個字,就會 setValue 一次,所以就會 re-render 一次 -> 重新執行一次 App()
但是,我在 input 欄位輸入字,跟 filteredTodos 沒有任何關係,所以 filteredTodos 其實不需要跟著重新計算一次
useMemo
在 derived state 外面包一個 useMemo
使用 useMemo 之後,這個 derived state 就不需要每次 re-render 都跟著重新計算一次,只有在「depedency array 裡面的 state 改變時」,才會重新計算一次
useMemo 裡面要傳入一個 function,然後要記得 return 東西
在 useMemo 錯誤使用 dependency array 的做法
在 useMemo 要傳入第二個參數(depedency array),如果第二個參數是傳一個「空陣列」的話,那就只有第一次 render 會計算 filteredTodos(印出 "calculate filteredTodos"),之後的 re-render 都不會再計算一次 filteredTodos 了
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,把 useMemo 的 dependency array 傳一個空陣列,這樣其實是錯的
原因為:
用一個「清空」的按鈕來說明,就知道為什麼是錯的了
我在 input 欄位旁邊放上一個「清空」的按鈕,點擊就會把 todos 變為空陣列
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, []);
console.log("render~");
return (
<div>
todo:{" "}
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但是,當我點擊「清空」按鈕後,會發現:頁面上的 todos 還在,並沒有被清空
其實,todos 陣列已經變成空陣列了(App() 有 re-render)
但是因為在 filteredTodos 的 useMemo 的 dependency array 是空陣列,任何的 state 改變都不會讓 filteredTodos 重新計算(依然還是舊的值),所以畫面並不會改變
但是,因為 filteredTodos 是透過 filter 和 todos 計算而成的,所以當 filter 或 todos 改變時,filteredTodos 應該要跟著改變才對
在 useMemo 正確使用 dependency array 的做法
所以,要在 useMemo 的 dependency array 裡面填入 filter, todos,意思就是:只有當 filter 或 todos 改變時,filteredTodos 才會重新計算一次
如果 filter 或 todos 都沒有改變,那 useMemo 會幫我記住「上一次計算的值」,所以不需要再計算一次
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
用 useCallback 記住 function
在 useCallback 錯誤使用 dependency array 的做法
當我在 input 欄位輸入字時,會觸發 onChange 事件並執行 handleChange
在 handleChange 裡面會把 todos 陣列印出來
export default function App() {
const [value, setValue] = useState("");
const [todos, setTodos] = useState([
{ id: 1, content: "aaa", isChecked: false },
{ id: 2, content: "bbb", isChecked: true },
]);
const [filter, setFilter] = useState("active");
// 可以從 filter 和 todos 來推算出 filteredTodos,所以 filteredTodos 是 derived state
const filteredTodos = useMemo(() => {
console.log("calculate filteredTodos");
return todos.filter((todo) => {
if (filter === "all") return true;
if (filter === "active") return !todo.isChecked;
if (filter === "completed") return todo.isChecked;
});
}, [filter, todos]);
const handleChange = useCallback((e) => {
console.log(todos);
setValue(e.target.value);
}, []);
console.log("render~");
return (
<div>
todo: <input value={value} onChange={handleChange} />
<button
onClick={() => {
setTodos([]);
}}
>
清空所有項目
</button>
{filteredTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
在 useCallback 的 dependency array 傳一個空陣列,這是錯誤的寫法
因為:
dependency array 傳一個空陣列代表「只有第一次 render 會產生新的 function,之後就會一直沿用這個 function」
useCallback 會幫我記住上一個 function,所以就算 todos 的 state 改變了,因為 handleChange 還是第一次 render 時的 function,所以在 function 裡面的 todos 就還是第一次 render 時的值,這樣就是錯的
在 useCallback 正確使用 dependency array 的做法
因為在 handleChange 裡面有用到 todos,所以要在 dependency array 裡面填入 todos,這樣每一次 re-render 之後才會重新宣告一個新的 handleChange function,才會拿到最新的 todos 的值
const handleChange = useCallback(
(e) => {
console.log(todos);
setValue(e.target.value);
},
[todos]
);
總結
useMemo, useCallback, useEffect 的 dependency array 機制都是一樣的
![[Note] JS: Hoisting & TDZ](https://static.coderbridge.com/images/covers/default-post-cover-1.jpg)

