參考文章
遷移狀態(tài)邏輯至 Reducer 中
對于擁有許多狀態(tài)更新邏輯的組件來說,過于分散的事件處理程序可能會令人不知所措。對于這種情況,可以將組件的所有狀態(tài)更新邏輯整合到一個外部函數(shù)中,這個函數(shù)叫作 reducer。
使用 reducer 整合狀態(tài)邏輯
隨著組件復(fù)雜度的增加,將很難一眼看清所有的組件狀態(tài)更新邏輯。例如,下面的 TaskApp
組件有一個數(shù)組類型的狀態(tài) tasks
,并通過三個不同的事件處理程序來實(shí)現(xiàn)任務(wù)的添加、刪除和修改:
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
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));
}
return (
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{id: 0, text: '參觀卡夫卡博物館', done: true},
{id: 1, text: '看木偶戲', done: false},
{id: 2, text: '打卡列儂墻', done: false},
];
這個組件的每個事件處理程序都通過 setTasks
來更新狀態(tài)。隨著這個組件的不斷迭代,其狀態(tài)邏輯也會越來越多。為了降低這種復(fù)雜度,并讓所有邏輯都可以存放在一個易于理解的地方,可以將這些狀態(tài)邏輯移到組件之外的一個稱為 reducer 的函數(shù)中。
Reducer 是處理狀態(tài)的另一種方式??梢酝ㄟ^三個步驟將 useState
遷移到 useReducer
:
- 將設(shè)置狀態(tài)的邏輯 修改 成 dispatch 的一個 action;
- 編寫 一個 reducer 函數(shù);
- 在組件中 使用 reducer。
第 1 步: 將設(shè)置狀態(tài)的邏輯修改成 dispatch 的一個 action
事件處理程序目前是通過設(shè)置狀態(tài)來 實(shí)現(xiàn)邏輯的:
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));
}
移除所有的狀態(tài)設(shè)置邏輯。只留下三個事件處理函數(shù):
-
handleAddTask(text)
在用戶點(diǎn)擊 “添加” 時被調(diào)用。 -
handleChangeTask(task)
在用戶切換任務(wù)或點(diǎn)擊 “保存” 時被調(diào)用。 -
handleDeleteTask(taskId)
在用戶點(diǎn)擊 “刪除” 時被調(diào)用。
使用 reducers 管理狀態(tài)與直接設(shè)置狀態(tài)略有不同。它不是通過設(shè)置狀態(tài)來告訴 React “要做什么”,而是通過事件處理程序 dispatch 一個 “action” 來指明 “用戶剛剛做了什么”。(而狀態(tài)更新邏輯則保存在其他地方?。┮虼耍辉偻ㄟ^事件處理器直接 “設(shè)置 task
”,而是 dispatch 一個 “添加/修改/刪除任務(wù)” 的 action。這更加符合用戶的思維。
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
傳遞給 dispatch
的對象叫做 “action”:
function handleDeleteTask(taskId) {
dispatch(
// "action" 對象:
{
type: 'deleted',
id: taskId,
}
);
}
它是一個普通的 JavaScript 對象。它的結(jié)構(gòu)是由你決定的,但通常來說,它應(yīng)該至少包含可以表明 發(fā)生了什么事情 的信息。
注意:action 對象可以有多種結(jié)構(gòu)。
按照慣例,通常會添加一個字符串類型的 type
字段來描述發(fā)生了什么,并通過其它字段傳遞額外的信息。type
是特定于組件的,在這個例子中 added
和 addded_task
都可以。選一個能描述清楚發(fā)生的事件的名字!
dispatch({
// 針對特定的組件
type: 'what_happened',
// 其它字段放這里
});
第 2 步: 編寫一個 reducer 函數(shù)
reducer 函數(shù)就是放置狀態(tài)邏輯的地方。它接受兩個參數(shù),分別為當(dāng)前 state 和 action 對象,并且返回的是更新后的 state:
function yourReducer(state, action) {
// 給 React 返回更新后的狀態(tài)
}
React 會將狀態(tài)設(shè)置為從 reducer 返回的狀態(tài)。
在這個例子中,要將狀態(tài)設(shè)置邏輯從事件處理程序移到 reducer 函數(shù)中,需要:
- 聲明當(dāng)前狀態(tài)(
tasks
)作為第一個參數(shù); - 聲明
action
對象作為第二個參數(shù); - 從
reducer
返回 下一個 狀態(tài)(React 會將舊的狀態(tài)設(shè)置為這個最新的狀態(tài))。
下面是所有遷移到 reducer
函數(shù)的狀態(tài)設(shè)置邏輯:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('未知 action: ' + action.type);
}
}
由于 reducer
函數(shù)接受 state
(tasks)作為參數(shù),因此可以 在組件之外聲明它。這減少了代碼的縮進(jìn)級別,提升了代碼的可讀性。
注意:上面的代碼使用了 if/else
語句,但是在 reducers 中使用 switch 語句 是一種慣例。兩種方式結(jié)果是相同的,但 switch
語句讀起來一目了然。
以后會像這樣使用:
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('未知 action: ' + action.type);
}
}
}
建議將每個 case
塊包裝到 {
和 }
花括號中,這樣在不同 case
中聲明的變量就不會互相沖突。此外,case
通常應(yīng)該以 return
結(jié)尾。如果忘了 return
,代碼就會 進(jìn)入
到下一個 case
,這就會導(dǎo)致錯誤!
如果還不熟悉 switch
語句,使用 if/else
也是可以的。
第 3 步: 在組件中使用 reducer
最后,需要將 tasksReducer
導(dǎo)入到組件中。記得先從 React 中導(dǎo)入 useReducer
Hook:
import { useReducer } from 'react';
接下來,就可以替換掉之前的 useState
:
const [tasks, setTasks] = useState(initialTasks);
只需要像下面這樣使用 useReducer
:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
和 useState
很相似——必須給它傳遞一個初始狀態(tài),它會返回一個有狀態(tài)的值和一個設(shè)置該狀態(tài)的函數(shù)(在這個例子中就是 dispatch 函數(shù))。但是,它們兩個之間還是有點(diǎn)差異的。
useReducer
鉤子接受 2 個參數(shù):
- 一個 reducer 函數(shù)
- 一個初始的 state
它返回如下內(nèi)容:
- 一個有狀態(tài)的值
- 一個 dispatch 函數(shù)(用來 “派發(fā)” 用戶操作給 reducer)
現(xiàn)在一切都準(zhǔn)備就緒了!在這里把 reducer 定義在了組件的末尾:
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
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,
});
}
return (
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
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('未知 action: ' + action.type);
}
}
}
...
如果有需要,甚至可以把 reducer 移到一個單獨(dú)的文件中:
// tasksReducer.js
export default 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('未知 action:' + action.type);
}
}
}
當(dāng)像這樣分離關(guān)注點(diǎn)時,我們可以更容易地理解組件邏輯?,F(xiàn)在,事件處理程序只通過派發(fā) action
來指定 發(fā)生了什么,而 reducer
函數(shù)通過響應(yīng) actions
來決定 狀態(tài)如何更新。
對比 useState 和 useReducer
Reducers 并非沒有缺點(diǎn)!以下是比較它們的幾種方法:
-
代碼體積: 通常,在使用
useState
時,一開始只需要編寫少量代碼。而useReducer
必須提前編寫 reducer 函數(shù)和需要調(diào)度的 actions。但是,當(dāng)多個事件處理程序以相似的方式修改 state 時,useReducer
可以減少代碼量。 -
可讀性: 當(dāng)狀態(tài)更新邏輯足夠簡單時,
useState
的可讀性還行。但是,一旦邏輯變得復(fù)雜起來,它們會使組件變得臃腫且難以閱讀。在這種情況下,useReducer
允許將狀態(tài)更新邏輯與事件處理程序分離開來。 -
可調(diào)試性: 當(dāng)使用
useState
出現(xiàn)問題時, 很難發(fā)現(xiàn)具體原因以及為什么。 而使用useReducer
時, 可以在 reducer 函數(shù)中通過打印日志的方式來觀察每個狀態(tài)的更新,以及為什么要更新(來自哪個action
)。 如果所有action
都沒問題,就知道問題出在了 reducer 本身的邏輯中。 然而,與使用useState
相比,必須單步執(zhí)行更多的代碼。 -
可測試性: reducer 是一個不依賴于組件的純函數(shù)。這就意味著可以單獨(dú)對它進(jìn)行測試。一般來說,我們最好是在真實(shí)環(huán)境中測試組件,但對于復(fù)雜的狀態(tài)更新邏輯,針對特定的初始狀態(tài)和
action
,斷言 reducer 返回的特定狀態(tài)會很有幫助。 -
個人偏好: 并不是所有人都喜歡用 reducer,沒關(guān)系,這是個人偏好問題??梢噪S時在
useState
和useReducer
之間切換,它們能做的事情是一樣的!
如果在修改某些組件狀態(tài)時經(jīng)常出現(xiàn)問題或者想給組件添加更多邏輯時,建議還是使用 reducer。當(dāng)然,也不必整個項(xiàng)目都用 reducer,這是可以自由搭配的。甚至可以在一個組件中同時使用 useState
和 useReducer
。
編寫一個好的 reducers
編寫 reducers
時最好牢記以下兩點(diǎn):
-
reducers 必須是純粹的。 這一點(diǎn)和 狀態(tài)更新函數(shù) 是相似的,
reducers
在是在渲染時運(yùn)行的?。╝ctions 會排隊(duì)直到下一次渲染)。 這就意味著reducers
必須純凈,即當(dāng)輸入相同時,輸出也是相同的。它們不應(yīng)該包含異步請求、定時器或者任何副作用(對組件外部有影響的操作)。它們應(yīng)該以不可變值的方式去更新 對象 和 數(shù)組。 -
每個 action 都描述了一個單一的用戶交互,即使它會引發(fā)數(shù)據(jù)的多個變化。 舉個例子,如果用戶在一個由
reducer
管理的表單(包含五個表單項(xiàng))中點(diǎn)擊了重置按鈕
,那么 dispatch 一個reset_form
的 action 比 dispatch 五個單獨(dú)的set_field
的 action 更加合理。如果在一個reducer
中打印了所有的action
日志,那么這個日志應(yīng)該是很清晰的,它能讓你以某種步驟復(fù)現(xiàn)已發(fā)生的交互或響應(yīng)。這對代碼調(diào)試很有幫助!
使用 Immer 簡化 reducers
與在平常的 state 中 修改對象 和 數(shù)組 一樣,可以使用 Immer
這個庫來簡化 reducer
。在這里,useImmerReducer
讓你可以通過 push
或 arr[i] =
來修改 state :文章來源:http://www.zghlxwxcb.cn/news/detail-690151.html
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false,
});
break;
}
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);
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,
});
}
return (
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
...
Reducers 應(yīng)該是純凈的,所以它們不應(yīng)該去修改 state。而 Immer 提供了一種特殊的 draft
對象,可以通過它安全的修改 state。在底層,Immer 會基于當(dāng)前 state 創(chuàng)建一個副本。這就是為什么通過 useImmerReducer
來管理 reducers 時,可以修改第一個參數(shù),且不需要返回一個新的 state 的原因。文章來源地址http://www.zghlxwxcb.cn/news/detail-690151.html
摘要
- 把
useState
轉(zhuǎn)化為useReducer
:- 通過事件處理函數(shù) dispatch actions;
- 編寫一個 reducer 函數(shù),它接受傳入的 state 和一個 action,并返回一個新的 state;
- 使用
useReducer
替換useState
;
- Reducers 可能需要寫更多的代碼,但是這有利于代碼的調(diào)試和測試。
- Reducers 必須是純凈的。
- 每個 action 都描述了一個單一的用戶交互。
- 使用 Immer 來幫助在 reducer 里直接修改狀態(tài)。
到了這里,關(guān)于React 18 遷移狀態(tài)邏輯至 Reducer 中的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!