一、React 基礎總結
TodoList 的時候,比較注重在 component 之間的互動,以及基本功能的使用
- Component
- Props
- Style
- Event handler
- JSX
- state
接著關注 React 底層的一些機制,還有一些舊的東西、useEffect、life-cycle
- useEffect:要執行事情,要馬寫在 useEffect 裡面,要馬寫在 Event handler 裡面,因為寫在其他地方每次 render 就會跟著一起執行
- render 機制:避免 re-render 就和 3 4 5 有關
- useCallback:背後透過 useRef 實作,useRef 在不同的 render 之間保留一個 reference,功能同 useCallback
- useMemo:用一個 function 將你想要的值回傳回來
- memo:包 component
- Class Component:舊專案還是會看到,function component 可以被相關的 useEffect 取代
- useContext:解決 props drilling
建議將官方文件每一頁看過,至少對東西有印象,不求都了解,但要能知道這個名次、他在做甚麼、能用一句話簡單講出來。以後碰到類似問題,就可以知道去哪裡解決。
二、增進代碼品質
- eslint 開啟
- propType:檢查 prop 的 type 是否正確,因為 js 並沒有規定你只能寫甚麼型態,比方說今天 render 一個
this.props.nam
,他可能是數字、陣列、物件...,所以 react 規定 component.propTypes = 一個物件,就可以去規定說這個 props 他吃的名字是甚麼、是甚麼型態。使用若不照這個規則,就會在 console 顯示 warning。會提示你 props 好像傳錯了。
要使用方式 2 需要改一下 eslint 的規則,在根目錄新增檔案 .eslintrc.json
,
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string
};
react props type 的 rule,每個 component 都一定要加有甚麼 props。寫起來頗麻煩,因為每個 component 都要先寫好。
// TodoItem.js
import PropTypes from 'prop-types' // react 提供幫助我們定義已經有的類型
略
TodoItem.propTypes = {
className: PropTypes.string,
size: PropTypes.string.isRequired, // 一定要傳,不然 console 會報錯
todo: PropTypes.object, // 也可以定義更詳細,裡面到底要有甚麼東西
handleDeleteTodo: PropTypes.func,
hadnleToggleIsDone: PropTypes.func
}
好處:快速知道這個 component 有甚麼參數,這些參數需要傳遞的類型是甚麼。某些 library 也會用這個文件產生東西出來。
三、補充:以 state 為中心去思考
在寫 react 時一定要做一個想法的轉換:以資料為中心思考
今天看到畫面、有資訊(資料格式可以先想成 json),如果要保畫面的資料保存或是下載,如果這個 data 可以幫我將畫面還原,他需要有甚麼資訊?
舉例:todos 而言,缺少下列任一東西都會無法還原畫面
- 紀錄輸入的值是甚麼
- 代辦事項 todos:應該是一個陣列,裡面每個 todo 是一個物件(todo 內容、以勾選/未勾選)
- 每個 todo 會希望給他一個流水號的 id,他就不會被放在 state 裡面,因為當 todo id 改變時,畫面不需要變動 => 用 useRef 存
將 state 規劃好可以與畫面進行對照,畫面未必要與 state 完全一比一,也就是說有些 state 不是必要的
資料改變會需要改變畫面的就是 state;需要改變但不影響畫面 useRef
{
inputValue: 'abcde',
todos: [
{
isChecked: true,
content: 'ddw',
},
{
isChecked: false,
content: 'lob'
}
],
itemLeftCount: 1,
filter: active
}
todoId: 2
state 分兩種
- state
- derived(衍伸) state:可以透過現有的 state 組合或計算而成,不應該放到 state 裡面。例如:itemLeftCount,只要在 todos 看有多少筆資料的 isChecked 是 false,最後得出的數字就是這個數字
放在 state 裡面的都是必要的,所以 inputValue、todos 都是必要的,但 derived state 非必要的,他可以透擴 state 的東西計算出來
不要將 derived state 放在 state 裡面會變得非常難維護
ex:
filterTodos 可以從 todos 和 filters 兩個狀態推測出來最後的 filterTodos 長相
// 改了 filter 顯示出來的東西就改變
const [filter, setFilter] = useState("active");
import "./styles.css";
import { useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{ content: "todo1", id: 1, isChecked: true },
{ content: "todo2", id: 2, isChecked: false }
]);
const [filter, setFilter] = useState("active");
const filterTodos = todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
});
return (
<div>
{filterTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
這樣不是每次 re-render 都要重新計算 ? 就算沒有改 todos 也沒有改 filter 今天有其他 state,例如:每次輸入數字就加一,因為每打一次就 setState 每次 setState 就觸發 re-render,重新呼叫 App
import "./styles.css";
import { useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{ content: "todo1", id: 1, isChecked: true },
{ content: "todo2", id: 2, isChecked: false }
]);
const [filter, setFilter] = useState("active");
const [value, setValue] = useState("");
const filterTodos = todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
});
console.log("render");
return (
<div>
todo: <input value={value} onChange={(e) => setValue(e.target.value)} />
{filterTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
但打字並不影響 filterTodos,所以這個不用再計算一次,這種計算的東西透過 useMemo 去處理。
// 用 useMemo 包起來打字還是會印出 calculate todo,因為第二個參數 dependency
import { useState, useMemo } from "react";
const filterTodos = useMemo(() => {
console.log('calculate todo')
return todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
})
});
console.log("render");
[] 表示沒有 dependency,之後怎麼改都不會觸發 calculate todo
const filterTodos = useMemo(() => {
console.log("calculate todo");
return todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
});
}, []);
上面的狀況以下面的代碼說明:按清空按鈕東西並沒有重新 render,但事實上 todos 有 re-render 他已經變成空陣列了。
但 useMemo 寫成沒有 dependency 所以 state 變化 useMemo 並不會重算一次。還會停留在剛剛的值。當我的 todos 改變 filterTodos 也要一起改變,因為 filterTodos 是透過 todos 根 filters 兩個組合而成的。
import "./styles.css";
import { useState, useMemo } from "react";
export default function App() {
const [todos, setTodos] = useState([
{ content: "todo1", id: 1, isChecked: true },
{ content: "todo2", id: 2, isChecked: false }
]);
const [filter, setFilter] = useState("active");
const [value, setValue] = useState("");
const filterTodos = useMemo(() => {
console.log("calculate todo");
return todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
});
}, []);
console.log("render");
return (
<div>
todo: <input value={value} onChange={(e) => setValue(e.target.value)} />
<button onClick={() => setTodos([])}>清空</button>
{filterTodos.map((todo) => (
<div key={todo.id}>{todo.content}</div>
))}
</div>
);
}
改成這樣,當 [todos, filter] 任一改變,filterTodos 都會重算一遍
const filterTodos = useMemo(() => {
console.log("calculate todo");
return todos.filter((todo) => {
if (filter === "all") return true;
return filter === "active" ? todo.isChecked : !todo.isChecked;
});
}, [todos, filter]);
useCallback 也是一樣,
當值改變將 todos 印出來,但將 todos 清空拿到的值還是一個陣列
使用 useCallback react 會將這個 function 給記住。可以將每次的 render 當作他重新 call 這個 function。
// useCallback 記憶 function
const handleChange = useCallback((e) => {
console.log(todos);
setValue(e.target.value);
}, []); // 沒有東西變了,我才要變 ie 任何東西改變我都不會變,都拿之前已經存好的 function 過來
useEffect 同理
小結
- 看畫面上有那些東西,所以我那些資料是必備的
- 可以透過 state 算出或組合出來的就是 derived state,對這類 state 外面可以包 useMemo 就不用每次都算,只有特定資料改變他才需要重新計算
- useCall, useEffect 第二個參數也是同理
- eslint 會提醒,9 成情況會需要填入她叫你填的東西
useRef 需要改變但不影響畫面,