使用 React Hooks + Context 打造簡版 Redux
React Hooks
在 [email protected] 版本正式釋出。我最近在一兩個公司的內部專案中也開始用起來嚐嚐鮮。
不瞭解Hooks
的同學先擼一遍文件。本文不對Hooks
做詳細介紹,只闡述一種使用Hooks
的思路。
一般我們寫React
如果不是特別大的應用,前後端資料互動邏輯不復雜,這樣我們直接按照正常流程寫元件就能滿足簡單的業務場景。隨著業務場景的深入漸漸地我們元件變大變多,元件與元件之間的資料通訊(也就是狀態管理,不過我更願意稱之為資料通訊)變得越來越複雜。所以我們引入了Redux
來維護我們日趨複雜的資料通訊。
思路
秉承著這種思路,我在開發應用的時候是沒有一開始就引入Redux
,因為一開始我覺得就是個小專案。隨著深入專案的開發,其實並沒有這麼簡單。
但是也沒有太複雜,這時我把眼光放到了Context
身上。Context
本意是上下文,它提供一個Provider
和一個Consumer
,這裡和Angular
裡的Provider
有點類似,也就是生產者/消費者模式,在某個頂層提供一個Provider
,下面的子元素通過Consumer
來消費Provider
裡的資料和方法。
通過這個概念,我們把不同層級裡的元件共享同一個頂層Provider
,並且元件內部使用Consumer
來消費共享資料。
當我們能共享資料後,還剩一個問題就是如何更改Provider
裡的資料呢?答案是:useReducer
。
好,有了思路,我們來實現一下。
例項
假設我們在某一個層級有個需要共享狀態的父級元素,我們稱它為 Parent,在 Parent 下面不同層級之間有兩個 Child。這裡為了簡單舉例假設兩個 Child 內都是共同的邏輯。
import React from "react" function Parent() { const colors = ['red', 'blue'] return ( <> <Child1 color={colors[0]} /> <Child2 color={colors[1]} /> </> ) } function Child1(props) { return ( <div style={{ background: props.color }}>I am {props.color}</div> ) } function Child2(props) { return ( <div style={{ background: props.color }}>I am {props.color}</div> ) } 複製程式碼
我們現在已經構造出了這樣的一個上下級結構,目前通過給子元件傳遞屬性,可以實現父元件的狀態共享。但是這裡如果層級加深,我們傳遞屬性的層級也要跟著加深。這樣顯然不是我們想要的。
現在我們來引入Context
。
首先通過createContext
方法初始化我們需要的Context
。
import React, { createContext } from "react" const Context = createContext({ colors: ['red', 'blue'] }) 複製程式碼
然後我們在 Parent 和 Child 裡引入剛才的Context
,並且使用useContext
拿到共享的資料:
import React, { useContext, createContext } from "react" const Context = createContext({ colors: [] }) function Parent() { const initState = { colors: ["red", "blue"] } return ( <Context.Provider value={{ colors: initState.colors }}> <> {/* 假裝這些地方有著不同的層級 */} <Child1 /> <Child2 /> </> </Context.Provider> ) } function Child1(props) { const { colors } = useContext(Context); return ( <div style={{ background: colors[0] }}> I am {colors[0]} </div> ) } // 省略 Child2 程式碼,同 Child1 一致 複製程式碼
現在只是拿到了資料並且進行渲染,再進一步,通過點選元素,修改顏色。在這裡我們就需要用useReducer
來模擬觸發改變。
首先我們需要一個reducer 來處理觸發的改變。
function reducer(state, action) { const { colors } = action if (action.type === "CHANGE_COLOR") { return { colors: colors } } else { throw new Error() } } 複製程式碼
這裡我簡化了action 的處理,當然你也可以進行擴充套件。
現在,我們給Provider
加上提供改變的方法dispatch
。
import React, { useContext, createContext } from "react" const Context = createContext({ colors: [] }) function Parent() { const initState = { colors: ["red", "blue"] } const [state, dispatch] = useReducer(reducer, initState) return ( <Context.Provider value={{ colors: state.colors, dispatch: dispatch }}> <> {/* 假裝這些地方有著不同的層級 */} <Child1 /> <Child2 /> </> </Context.Provider> ) } 複製程式碼
然後子元件觸發改變:
function Child1(props) { const { colors, dispatch } = useContext(Context) return ( <div style={{ background: colors[0] }} onClick={() => dispatch({ type: "CHANGE_COLOR", colors: ["yellow", "blue"] }) } > I am {colors[0]} </div> ) } // 省略 Child2 程式碼,同 Child1 一致 複製程式碼
至此,這個小型的狀態共享便完成了。這便是我們擺脫Redux
之後實現的狀態共享思路的雛形。完整的程式碼及例子見tiny redux。
進階
在實際的應用中,我們的業務場景會更復雜,比如我們的資料是動態獲取的。
這種情況下你可以把Provider
抽出來,當 Parent 資料回來之後再初始化Context
。
function Provider (props) { const { colors } = props const initState = { colors, } const [state, dispatch] = useReducer(reducer, initState) return ( <Context.Provider value={{ colors: state.colors, dispatch: dispatch }}> {props.children} </Context.Provider> ) } 複製程式碼
然後我們在 Parent 中做非同步操作,並把動態資料傳給Provider :
import React, { useState, useEffect } from "react" function Parent (props) { const [data, setData] = useState() const [url, setUrl] = useState('https://example.com') useEffect(() => { fetch(url).then(res => setData(data)) }, [url]) if (!data) return <div>Loading ...</div> return ( <Provider colors={data}> <> {/* 假裝這些地方有著不同的層級 */} <Child1 /> <Child2 /> </> </Provider> ) } 複製程式碼
結語
這樣小型的狀態管理機制你甚至可以放在某個元件裡,而不用放到如Redux
全域性的環境中去。這樣使得我們寫的應用更加靈活,而不是一味的往store
裡丟狀態。當然你也可以寫一個AppProvider
來管理全域性的狀態,React Hooks
+Context
給了我們這樣的便利。