原文:Getting to Know the useReducer React Hook
作者:Kingsley Silas
譯者:博軒
useReducer
是 React 16.8.0 中爲數不多由官方提供的 React Hook 之一。它接受一個 reducer 函數 ,以及一個初始的應用程序狀態,然後返回當前應用程序的狀態,和一個調度函數(dispatch)。
一個簡單的例子:
const [state, dispatch] = useReducer(reducer, initialState);
這樣有什麼好處?一個好主意是讓我們試着想象一下,一個應用初次加載屬性時的所有情況。它可能是可交互式的地圖上的一個起點。或許是允許用戶使用一個默認的模型來自定義選項,構建一個自定義汽車。這裏有一個非常簡潔的計算器,當計算器重置時,使用 useReducer
來使應用程序恢復默認狀態。
https://codepen.io/dpgian/emb...
我們將在這篇文章中深入研究幾個例子,瞭解一下 useReducer Hook
本身,以及應該何時使用。
全能的 reducer
說起 useState
,就不得不提及 JavaScript 的 reduce
方法。最開始,我們很難將它們聯繫起來,但是 Sarah 的一篇關於 reducer
的文章 可以幫助我們更好的理解。
關於reducer
最重要的一點就是:它每次只返回一個值。reducer
的工作就是減少。那個值可以是數字,字符串,對象,數組或者對象,但是它總是一個值。reducer
在很多情況下都很有效,但是他對於處理輸入一組值,返回一個值的情況非常有用。
假設我們有一個數字數組,reduce
將依次累加每一個值。這是數組:
const numbers = [1, 2, 3]
...以及一個函數,每次 reducer
中的計算都會在控制檯打印出來。這有助於我們理解 reducer
將數組提取爲單個數字的過程。
const reducer = function (tally, number) {
console.log(`Tally: ${tally}, Next number: ${number}, New Total: ${tally + number}`)
return tally + number
}
現在,讓我們運行一個 reducer
。正如我們所看到的,reduce
接收一個調度函數,以及一個初始狀態。讓我們傳入一個 reducer
函數,以及一個初始值:0。
const total = numbers.reduce(reducer, 0)
這是控制檯打印的內容:
"Tally: 0, Next number: 1, New Total: 1"
"Tally: 1, Next number: 2, New Total: 3"
"Tally: 3, Next number: 3, New Total: 6"
看 reduce
是如何將一個初始值累加,得到我們的最終結果的。在這個例子中,最終結果是 6。
我也十分喜歡 Dave Ceddia
的示例 ,他展示瞭如何使用 reduce
來拼寫一個單詞:
var letters = ['r', 'e', 'd', 'u', 'c', 'e'];
// `reduce` takes 2 arguments:
// - a function to do the reducing (you might say, a "reducer")
// - an initial value for accumulatedResult
var word = letters.reduce(
function(accumulatedResult, arrayItem) {
return accumulatedResult + arrayItem;
},
''); // <-- notice this empty string argument: it's the initial value
console.log(word) // => "reduce"
使用 useReducer ,states ,actions 一起工作
好的,接下來到了這篇文章的重點: useReducer
。到了這裏的一切都很重要,因爲使用 reduce
調用一個函數來處理初始值的方式,就是我們接下來的目標。它是同一種概念,但是會返回一個數組包含兩個元素,當前的狀態和調度函數。
const [state, dispatch] = useReducer(reducer, initialArg, init);
第三個參數init
是什麼?它是一個可選值,可以用來惰性提供初始狀態。這意味着我們可以使用使用一個init
函數來計算初始狀態/值,而不是顯式的提供值。如果初始值可能會不一樣,這會很方便,最後會用計算的值來代替初始值。
爲了使它工作,我們需要做一些事情:
- 定義初始狀態
- 提供一個包含
action
的函數來更新state
- 觸發
useReducer
,基於初始值計算並更新state
。
計數器就是一個經典的例子。事實上這也是官方文檔使用這個例子的原因:
https://codepen.io/kinsomicro...
這是一個很好的例子,因爲它演示了每次通過單擊增加或減少按鈕觸發操作時如何使用初始狀態(零值)來計算新值。我們甚至可以在其中輸入一個“重置”按鈕,將總數恢復到初始狀態零。
示例:汽車定製器
https://codepen.io/geoffgraha...
在此示例中,我們假設用戶已經選擇了自己要購買的汽車。但是,我們希望用戶可以爲汽車添加額外的選項。每個選項的價格都會影響汽車的總價。
首先,我們需要創建初始狀態,其中包括汽車,可以跟蹤功能的空數組 features
,$26,395 的起始價格 price
,一個存放未選配件的列表 store
,用戶可以選擇他們想要的東西。
const initialState = {
additionalPrice: 0,
car: {
price: 26395,
name: "2019 Ford Mustang",
image: "https://cdn.motor1.com/images/mgl/0AN2V/s1/2019-ford-mustang-bullitt.jpg",
features: []
},
store: [
{ id: 1, name: "V-6 engine", price: 1500 },
{ id: 2, name: "Racing detail package", price: 1500 },
{ id: 3, name: "Premium sound system", price: 500 },
{ id: 4, name: "Rear spoiler", price: 250 }
]
};
我們的 reducer
功能將處理兩件事:添加和刪除新項目。
const reducer = (state, action) => {
switch (action.type) {
case "REMOVE_ITEM":
return {
...state,
additionalPrice: state.additionalPrice - action.item.price,
car: { ...state.car, features: state.car.features.filter((x) => x.id !== action.item.id)},
store: [...state.store, action.item]
};
case "BUY_ITEM":
return {
...state,
additionalPrice: state.additionalPrice + action.item.price,
car: { ...state.car, features: [...state.car.features, action.item] },
store: state.store.filter((x) => x.id !== action.item.id)
}
default:
return state;
}
}
當用戶選擇他想要的項目時,我們更新汽車的 features
,增加 additionalPrice
並從商店中刪除該項目。我們確保 state
其他部分會保持原樣。當用戶從功能列表中刪除項目時,我們會執行類似操作 - 減少額外價格,將項目返回到商店。
以下是App組件的代碼。
const App = () => {
const inputRef = useRef();
const [state, dispatch] = useReducer(reducer, initialState);
const removeFeature = (item) => {
dispatch({ type: 'REMOVE_ITEM', item });
}
const buyItem = (item) => {
dispatch({ type: 'BUY_ITEM', item })
}
return (
<div>
<div className="box">
<figure className="image is-128x128">
<img src={state.car.image} />
</figure>
<h2>{state.car.name}</h2>
<p>Amount: ${state.car.price}</p>
<div className="content">
<h6>Extra items you bought:</h6>
{state.car.features.length ?
(
<ol type="1">
{state.car.features.map((item) => (
<li key={item.id}>
<button
onClick={() => removeFeature(item)}
className="button">X
</button>
{item.name}
</li>
))}
</ol>
) : <p>You can purchase items from the store.</p>
}
</div>
</div>
<div className="box">
<div className="content">
<h4>Store:</h4>
{state.store.length ?
(
<ol type="1">
{state.store.map((item) => (
<li key={item.id}>\
<button
onClick={() => buyItem(item)}
className="button">Buy
</button>
{item.name}
</li>
))}
</ol>
) : <p>No features</p>
}
</div>
<div className="content">
<h4>
Total Amount: ${state.car.price + state.additionalPrice}
</h4>
</div>
</div>
</div>
);
}
調度操作會包含所選項的詳細信息。我們使用 action
的類型來確定 reducer
函數如何處理狀態的更新。您可以看到渲染視圖會根據您的操作而做出改變 - 從商店購買的商品會從商店中刪除,並添加到功能列表當中。此外,總金額也會更新。毫無疑問,我們可以對示例進行修改達到學習的目的。
我們可以使用 useState 來代替嗎?
聰明的讀者可能一直在想這個問題。我的意思是,setState
大致會做相同的事情,不是嗎?返回一個具備狀態的值,以及一個可以使用新值重新渲染組件的函數。
const [state, setState] = useState(initialState);
我們甚至可以使用 useState
來實現官方文檔中的計數器的例子。但是 useReducer
在處理複雜狀態的時候是最優解。Kent C. Dodds 寫了一個兩者之間的差異(雖然他經常使用 setState
)他提供了一個 useReducer
的最佳實踐:
當你一個元素中的狀態,依賴另一個元素中的狀態,最好使用useReducer
例如,你正在完成一個井字遊戲。你的組件中的 狀態被稱爲
squares
,它包含了左右方格,以及其中的值。
我的經驗是使用 useReducer
來處理複雜的狀態,尤其是初始狀態是基於其他元素生成的情況下。
等等,我們已經有 Redux 了!
如果你使用 Redux 工作,也會理解這裏所涉及的所有內容,因爲它的設計理念是通過 Context API 來存儲,傳遞組件之間的狀態 - 不必通過其他組件傳遞 props
來實現。
那麼, useReducer
取代 Redux 了嗎?不,我的意思是,你基本可以通過 useContext hook
來實現你自己的 Redux,但是,這並不代表 Redux 沒有用了,它仍然有許多其他的功能和優點值得考慮。
你會在哪裏使用 useReducer
?他是否有比 setState
更好的地方?也許你可以嘗試使用我們這裏介紹的想法來構建一些東西,下面是一些想法。
- 一個日曆,會展示今天的日期,但允許用戶選擇其他日期。還可以添加一個“今天”按鈕,幫助用戶返回到今天的日期。
- 您可以嘗試改進汽車示例 - 讓用戶擁有一個購物車列表。你可以爲它定義初始狀態,然後用戶可以添加他們想要的額外功能,並收取一定費用。這些功能可以是預定義的,也可以由用戶自定義。
本文已經聯繫原文作者,並授權翻譯,轉載請保留原文鏈接