我們在第一篇文章中介紹了JavaScript中的reducer以及他的一些特點,對reducer不熟悉的小伙伴可以先看看第一篇。
React Hook功能正式發布之后,允許在function component中擁有state和副作用(useEffect)。官方提供了兩種state管理的hook:useState、useReducer。下面我們會通過一系列Demo逐步說明如何使用useReducer管理state。
useState版login
我們先看看登錄頁常規的使用useState
的實現方式:
function LoginPage() {
const [name, setName] = useState(''); // 用戶名
const [pwd, setPwd] = useState(''); // 密碼
const [isLoading, setIsLoading] = useState(false); // 是否展示loading,發送請求中
const [error, setError] = useState(''); // 錯誤信息
const [isLoggedIn, setIsLoggedIn] = useState(false); // 是否登錄
const login = (event) => {
event.preventDefault();
setError('');
setIsLoading(true);
login({ name, pwd })
.then(() => {
setIsLoggedIn(true);
setIsLoading(false);
})
.catch((error) => {
// 登錄失敗: 顯示錯誤信息、清空輸入框用戶名、密碼、清除loading標識
setError(error.message);
setName('');
setPwd('');
setIsLoading(false);
});
}
return (
// 返回頁面JSX Element
)
}
上面Demo我們定義了5個state來描述頁面的狀態,在login函數中當登錄成功、失敗時進行了一系列復雜的state設置。可以想象隨著需求越來越復雜更多的state加入到頁面,更多的setState分散在各處,很容易設置錯誤或者遺漏,維護這樣的老代碼更是一個噩夢。
useReducer版login
下面看看如何使用useReducer改造這段代碼,先簡單介紹下useReducer。
const [state, dispatch] = useReducer(reducer, initState);
useReducer接收兩個參數:
第一個參數:reducer函數,沒錯就是我們上一篇文章介紹的。第二個參數:初始化的state。返回值為最新的state和dispatch函數(用來觸發reducer函數,計算對應的state)。按照官方的說法:對于復雜的state操作邏輯,嵌套的state的對象,推薦使用useReducer。
聽起來比較抽象,我們先看一個簡單的例子:
// 官方 useReducer Demo
// 第一個參數:應用的初始化
const initialState = {count: 0};
// 第二個參數:state的reducer處理函數
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
// 返回值:最新的state和dispatch函數
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
// useReducer會根據dispatch的action,返回最終的state,并觸發rerender
Count: {state.count}
// dispatch 用來接收一個 action參數「reducer中的action」,用來觸發reducer函數,更新最新的狀態
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
了解了useReducer基本使用方法后,看看如何使用useReducer改造上面的login demo:
const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default:
return state;
}
}
function LoginPage() {
const [state, dispatch] = useReducer(loginReducer, initState);
const { name, pwd, isLoading, error, isLoggedIn } = state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return (
// 返回頁面JSX Element
)
}
乍一看useReducer改造后的代碼反而更長了,但很明顯第二版有更好的可讀性,我們也能更清晰的了解state的變化邏輯。
可以看到login函數現在更清晰的表達了用戶的意圖,開始登錄login、登錄success、登錄error。LoginPage不需要關心如何處理這幾種行為,那是loginReducer需要關心的,表現和業務分離。
另一個好處是所有的state處理都集中到了一起,使得我們對state的變化更有掌控力,同時也更容易復用state邏輯變化代碼,比如在其他函數中也需要觸發登錄error狀態,只需要dispatch({ type: 'error' })。
useReducer可以讓我們將what
和how
分開。比如點擊了登錄按鈕,我們要做的就是發起登陸操作dispatch({ type: 'login' })
,點擊退出按鈕就發起退出操作dispatch({ type: 'logout' })
,所有和how
相關的代碼都在reducer中維護,組件中只需要思考What
,讓我們的代碼可以像用戶的行為一樣,更加清晰。
除此之外還有一個好處,我們在前文提過Reducer其實一個UI無關的純函數,useReducer的方案是的我們更容易構建自動化測試用例。
總結
最后我們總結一下這篇文章的一些主要內容:使用reducer的場景
- 如果你的
state
是一個數組或者對象 - 如果你的
state
變化很復雜,經常一個操作需要修改很多state - 如果你希望構建自動化測試用例來保證程序的穩定性
- 如果你需要在深層子組件里面去修改一些狀態(關于這點我們下篇文章會詳細介紹)
- 如果你用應用程序比較大,希望UI和業務能夠分開維護
這篇文章我們介紹了使用useReducer,幫助我們集中式的處理復雜的state管理。但如果我們的頁面很復雜,拆分成了多層多個組件,我們如果在子組件觸發這些state變化呢,比如在LoginButton觸發登錄操作? 我們將在下篇文章介紹如何處理復雜組件樹結構的reducer共享問題。
如果有小伙伴看過thinking-in-react可能會有疑問,官方不是推薦State應該有子組件自己維護么,為什么還要集中式的處理?
其實我們并不是推薦所有的state放一起,而是如果確實有很多state跨多個組件公用需要放到page級別維護,這時候可以考慮使用useReducer。
PS:推薦兩篇React State管理的文章
thinking-in-react沒看過的小伙伴墻裂推薦一定要看一遍,寫的非常的好。
最后慣例,歡迎大家star我們的人人貸大前端團隊博客,所有的文章還會同步更新到知乎專欄 和 掘金賬號,我們每周都會分享幾篇高質量的大前端技術文章。如果你喜歡這篇文章,希望能動動小手給個贊。
參考鏈接
- https://github.com/immerjs/immera
- https://reactjs.org/docs/context.html
- https://reactjs.org/docs/hooks-faq.html
- https://www.robinwieruch.de/react-usereducer-vs-usestate/
- https://www.robinwieruch.de/react-state-usereducer-usestate-usecontext/
- https://kentcdodds.com/blog/application-state-management-with-react