翻譯|How to Use the useReducer Hook
原文: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過時.