原文:How to Use the useReducer Hook
在所有的新React Hooks
,或許僅僅是因為名字,就可能成為使用最多的一個.
"reducer"這個單詞會讓很多人聯想起Redux-但是讀本文,你不必事先理解Redux.
我們這裡要談的"reducer"實際問題是,如何利用useReducer
的優點來管理元件中的複雜狀態(state),新的hook對於Redux意味著什麼?Redux需要hook嗎?(對不起,有點跑題).
[^譯註:結合Redux和useReducer來闡述問題,可能是一個很好的出發點, Redux的reducer和useReducer核心都是根據元件dispatch的Action的type,payload來對State物件進行更新.概念是完全一樣的,如果對Redux不是太瞭解, 可以藉助useReducer來理解這個過程. 留給你大腦的轉變過程是,如果兩者之間的這種相同點存在,可以遷移嗎?]
在本文中,我們會探討一下useReducer
.在元件中管理複雜state,要比useState
的方式厲害的多.
什麼是Reducer?
如果你熟悉Redux,或者陣列的reduce
方法,你就應該知道reducer 是什麼?
.如果你不熟悉,"reducer"是一個奇特的單詞,代表一個函式接收兩個值,返回一個值.
如果有一個陣列, 你想把其中的元素組合成單個值,"函數語言程式設計"的做法是使用陣列的reduce
函式. 例如,如果你有一個陣列,元素是數字,你想得到數字的綜合, 可以編寫一個reducer函式,傳遞給陣列的reduce
方法,例如:
let numbers = [1, 2, 3];
let sum = numbers.reduce((total, number) => {
return total + number;
}, 0);
複製程式碼
如果之前沒看過這樣的用法,可能有點暈. 這裡所做的是針對陣列的每個元素呼叫函式,傳遞的引數是前一個total
和當前的number
.函式返回值成為新的total
,第二個傳遞給reduce
的引數(在這裡是0)就是total
的初始值. 在這個例子中,輸入的函式將會呼叫三次:
- 用個(0,1)呼叫,返回
1
- 用個(
1
,2)呼叫,返回3 - 用個(
3
,3)呼叫,返回1 reduce
返回6,結果儲存在sum
中.
但是,這和useReducer有什麼關係?
我花了半頁的篇幅倆解釋陣列的reduce
的原因是因為,useReducer
接受相同的引數,基礎的工作是相同的.你傳遞一個reducer函式和初始值(initial state). reducer接收當前的state
和一個action
,返回一個新的state.我們可以寫一個類似的合計reducer:
useReducer((state,action)=>{
Return state+action;
},0)
複製程式碼
那麼如何觸發這個操作? action
是如何輸入函式的. 想到這個問題就對了.
[^譯註:這裡的這個問題絕對是學習Redux時,令人最困惑的地方]
useReducer
返回有兩個元素的陣列,類似useState hook. 第一個元素是當前的state,第二個引數是dispatch
函式. 實際的程式碼如下:
const [sum, dispatch] = useReducer((state, action) => {
return state + action;
}, 0);
複製程式碼
注意"state"可以是任何值,不一定非要是一個物件. 可以是數字,陣列,任何東西.
接著來看一個使用reducer的完整元件例項:
import React, { useReducer } from 'react';
function Counter() {
// 首次渲染會建立一個state,後續的渲染會儲存結果.
const [sum, dispatch] = useReducer((state, action) => {
return state + action;
}, 0);
return (
<>
{sum}
<button onClick={() => dispatch(1)}>
Add 1
</button>
</>
);
}
複製程式碼
可以在CodeSandbox
試試
可以看到,點選按鈕,dispatch一個action
,引數是1, 這個值會被加到當前的state上, 之後元件會用新的state(更大的值)來渲染元件.
我可以的把"action"寫成這樣.沒有使用{type:"INCREMENT_BY",value:1}
的形式或者其他類似Redux的形式,因為reducer不一定必須要準守Redux的type模式.Hooks的世界是一個全新的世界:這一點很值得考慮,是否能發現舊有模式的價值,並保持它們,還是使用新的模式.
稍微複雜一點的例子
現在來看一個和典型Redux reducer
非常接近的例項.我們要建立一個元件管理購物車列表,同時也會使用另一個hook:useRef
首先匯入兩個hook:
import React,{useReducer,useRef} from 'react'
複製程式碼
接著建立元件,設定ref和reducer.ref保留對錶單輸入的引用,便於我們獲取表單的值(也可以通過元件內部state,傳遞value
,onChange
props來獲取值,但是用useRef可以很好的展現它的用法)
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
// do something with the action
}
}, []);
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
</li>
))}
</ul>
</>
);
}
複製程式碼
注意,本例中的"state"是一個陣列.我們使用一個空陣列來初始化它,(傳遞給useReducer
的第二個引數),後續會從reducer函式返回一個陣列.
useRef Hook
題外話解釋一下useRef
的用法,之後在返回reducer話題.
useRef
hook 可以讓我們建立一個DOM元素的持久化引用. 呼叫useRef
會建立一個空的引用(可以傳遞引數進行初始化).返回的物件有一個current
屬性,所以在例項中,我們可以通過inputRef.current
來訪問DOM元素的輸入值. 如果你對React.createRef()
很熟悉,這裡的工作原理是相同的.
從useRef
返回的物件不僅僅可以承載一個DOM元素的引用,它可以承載做元件內的任何特定值,並且在渲染中保持固定.耳熟! 必須的.
useRef
也可用於建立泛型例項化變數,和React 類元件中的this.whatever=value
做法一樣. 唯一的區別是要寫成"side effect"的形式,所以就不能在元件渲染過程中改變它了-只能在useEffect
內部執行. 官方Hook問答
有例項講解.
回到useReducer的例子
用from
包裝input
,在按下Enter鍵時觸發提交函式. 現在需要編寫handleSubmit
函式,認為是把一個專案新增到列表上,還要在reducer中處理action
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
return [
...state,
{
id: state.length,
name: action.name
}
];
default:
return state;
}
}, []);
function handleSubmit(e) {
e.preventDefault();
dispatch({
type: 'add',
name: inputRef.current.value
});
inputRef.current.value = '';
}
return (
// ... same ...
);
}
複製程式碼
reducer函式有兩個分支: 一個是action:type==='add'
,預設
分支:其他的任務.
當reduce獲取到"add" action 以後, 它會返回一個新的陣列包含了舊的元素,在末尾新增新的一條專案.
我們使用陣列的長度作為自增ID.在這個例項中用自增ID是可以的,但是在實際的app中,不太理想,因為有可能導致重複的ID和bugs(最好是使用類似uuid
的軟體包,或者由伺服器生成一個唯一的ID!)
在使用者點選Enter鍵時,會呼叫handleSubmit
函式,所以需要呼叫preventDefault
來避免正頁面的過載. 之後呼叫dispatch
,引數是action.在app中,我們想讓action更像Redux形式-擁有type
屬性,附帶一些資料. 此外還有清除輸入.
這個階段的程式碼CodeSandBox
移除一項
現在新增從列表中移除專案的能力
挨著專案新增 刪除按鈕,點選時會dispatch一個action,引數是type==="remove"
,需要刪除專案的索引
接著需要在Reducer中處理action,通過過濾陣列來移除專案
function ShoppingList() {
const inputRef = useRef();
const [items, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add':
// ... same as before ...
case 'remove':
// keep every item except the one we want to remove
return state.filter((_, index) => index != action.index);
default:
return state;
}
}, []);
function handleSubmit(e) { /*...*/ }
return (
<>
<form onSubmit={handleSubmit}>
<input ref={inputRef} />
</form>
<ul>
{items.map((item, index) => (
<li key={item.id}>
{item.name}
<button
onClick={() => dispatch({ type: 'remove', index })}
>
X
</button>
</li>
))}
</ul>
</>
);
}
複製程式碼
這個階段的程式碼CodeSandBox
練習:清除列表
在額外新增一個內容,清空列表的按鈕,作為練習.
在<ul>
之上新增一個按鈕, 新增onClick
屬性,可以dispatch,type為"clear"的action.之後在reducer中新增分支處理"clear"action.
那麼... Redux就此終結篇章了嗎?
很多人初次看到useReducer
就想,React現在內建reducer了,還有Context可以在全域性範圍傳遞資料,所以Redux已死! 我想給出我的一些想法,因為我猜你也很想知道到Redux的命運將會如何?
[^譯註: 我個人觀點, useReducer的引入不僅不會讓Redux很難堪,反而會讓程式設計師藉助useReducer對Redux有更深的認識,Redux的構架學習可能會有很多的回報,此刻如果捨棄React-Native,投入flutter的懷抱, flutter-Redux的就不再是一個負擔了.]
我不認為useReducer
會殺死Redux,Context
也不會. 我認為這兩個方法只是擴充套件了React state管理的方法範圍而已,所以真正的情況是他們會減少使用Redux的用例.
Redux仍然比Context+useReducer
所做的工作多得多- Redux有Redux DevTools用於拍錯,可以定製化的元件,還有全生態系統的助手軟體包
.你可以大膽的說,Redux在很多情況下都有點殺雞用牛刀.但是我認為它仍然是非常強有力的.
Redux提供的全域性
store可以讓你集中控制app的data.useReducer
是特定元件私有的.使用useReducer
,useContext
構建一個迷你版的Redux也是完全可行的. 如果你想做,它們完全可以滿足需求(Twitter上有很多人已經做了,有截圖).我個人仍然想念DevTools.
總之-Redux活蹦亂跳的.Hooks不會讓Redux過時.
自己嘗試一下
一下是幾個小的應用,可以用useReducer
hook來完成
- 建一所房子,有一盞燈,按按鈕可以調光-關,低亮度,中等亮度,最高亮度
- 做一個鍵盤鎖,有6個按鈕, 正確的順序會解鎖. 真確的按鍵順序事先記錄在state中, 順序不正確會充值.