這一次徹底搞定useReducer-使用篇

useReducer-基礎概念篇

useReducer-使用篇

useReducer-配合useContext使用

我們在第一篇文章中介紹了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可以讓我們將whathow分開。比如點擊了登錄按鈕,我們要做的就是發起登陸操作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管理的文章

最后慣例,歡迎大家star我們的人人貸大前端團隊博客,所有的文章還會同步更新到知乎專欄掘金賬號,我們每周都會分享幾篇高質量的大前端技術文章。如果你喜歡這篇文章,希望能動動小手給個贊。

參考鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374