任務清單的實作中,可能有新增、修改、刪除的按鈕動作,如果使用 useState,三個事件處理函式都要設定 state,例如:
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
在handleAddTask
、handleChangeTask
、handleDeleteTask
都使用了 setTasks
去更改 task 內容。隨著元件的功能增加,程式碼可能會越變越複雜且不好維護。為了降低複雜度,且保持邏輯寫在同一個區塊,可以將這些狀態相關的邏輯移到一個函式中,這個函式稱為 reducer。
使用 reducer 管理狀態不是直接設定新的 state,而是在告訴 React 設定 state 要做什麽事。
原本是透過事件處理函式來更新 task 狀態,改寫成 task 有三種 action,added/changed/deleted (改用行為/動作作為名稱,也更貼近使用者的角度)
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
// 改寫成
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
不是直接設定 task,而是使用 dispatch action,將 action 傳入 dispatch 中。
(這裡不用告訴 React 新的 state 長什麽樣子,而是告訴 React 要做的事是 added,這件事具體要做什麽則寫在 reducer 中)
再來將 state 邏輯判斷寫在 reducer 函式中。函式傳入兩個參數,current state
和 action 物件
,函式會回傳下一個 state。
function yourReducer(state, action) {
// return next state for React to set
}
reducer 函式 return 的內容也就是 React 會設定的 state
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
最後在元件中加入 tasksReducer
// 載入 useReducer Hook
import { useReducer } from 'react';
// 原本使用 useState
// const [tasks, setTasks] = useState(initialTasks);
// 改為 useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer Hook 需要傳入兩個參數:
會回傳:
handleAddTask、handleChangeTask、handleDeleteTask 改用 dispatch 傳入 action
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
把可能要做的 action 統一寫在 reducer 函式
中,觸發時使用 dispatch()
告訴 reducer函式
是哪個 action type發生,資料內容有什麽,reducer 函式 return 的內容更新至 state 中。
Heading 元件會收到 level 值,識別為 h1~h6
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={5}>Sub-sub-sub-heading</Heading>
<Heading level={6}>Sub-sub-sub-sub-heading</Heading>
</Section>
);
}
同一個 section 包住的 heading 都使用一樣的 heading
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
期望可以改成由 section 設定統一的 level
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
必須讓子元件去 tree 中的某個地方詢問資訊,也就是 context,使用 context 可以將父元件將資訊傳給 UI tree 中任意一個元件。
import { createContext } from 'react';
export const LevelContext = createContext(1);
使用 context
在需要資料的元件中使用 context,就像 Heading 需要用到 LevelContext
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
跟 useState
和 useReducer
一樣也是 Hook,useContext 告訴 React Heading 元件想要讀取 LevelContext。
提供內容給 context
Section 元件 render children
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
context provider
包住 children,提供 LevelContext
給 children。用來告訴 React,如果有任何 <Section>
中有任何元件要 LevelContext
,就給它們 level 值,元件會使用最接近的 LevelContext.Provider
。 (沒有提供 context 時,React 會使用預設值)
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
context 容易被過度使用,如果要將 props 往不同層的元件傳遞,不代表一定要用 context,在使用 context 之前:
<Layout posts={posts} />
改為 <Layout><Posts posts={posts} /></Layout>
如果以上都不適用時,再去考慮 context!
React 從 0 到 0.1 (1)~(7) 都是 React 官方文件 的內容,官方提供的學習文件很清楚。也會以常見情境作為範例,再用同一個例子解釋可能遇到的問題,以及可以用什麽方式改寫。
每一個主題底下有很多單元,每個單元的內容長度很剛好,閱讀起來不會有太大的負擔。此外內容中也會夾雜 DEEP DIVE
、Pitfall
、Note
區塊,對實際應用很有幫助,每個單元最後也幾乎會有 Challenges,加深學習的記憶。