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 機制都是一樣的