Stories

Detail Return Return

如何採用React Context & Hooks 去做全局狀態管理? - Stories Detail

在業務或者UI交互稍微複雜一些的項目裏,都離不開狀態管理的問題。不管是從後台API請求的數據還是頁面的UI狀態,都需要有一個"Store"幫我們去做狀態管理。通常在項目中,我們會引入 Redux 去負責這樣的職責。但是 Redux 要維護大量的模板代碼,加上 Redux 通過 connect 這種高階組件的方式注入 state 和 dispatch 的方式並不直觀,增加了理解的複雜度。React從16.8開始,引入了 React Hook,配合 Context API,可以在項目中來解決全局狀態管理的問題。

React Context + Hooks 狀態管理架構

React Context + Hooks 狀態管理架構圖
我們先來整體看一下通過 React Context 和 Hooks 進行狀態管理的架構圖。我們把構成架構的每個負責不同職責的節點稱為"功能單元",下面列出了架構圖中涉及的所有功能單元和對應的職責:

  • React Context:負責全局狀態管理,通過提供的 Context API,可以進行狀態的讀寫。每個 Context 都會生成 Context.Provider 和 Context.Consumer

    • Context.Provider:為消費組件提供存儲的 context 值,通常存儲全局 state 和用來分發 action 的 dispatch 方法
    • Context.Consumer:可以訂閲 context 的變更,引入 Hooks 後,可以使用 useContext 來讀取 context,因此在實現中不需要關心 Context.Consumer
  • Component:組件的概念不需要多講,在基於組件的架構中,是用來構成 React 應用最基礎的功能單元
  • Action:Action 是組件發出的通知,用於描述要發生的事件或者變化
  • Reducer:用於響應 Action,來處理 state 的變化,基於 Action 和舊的 state 返回新的 state

在架構圖中,描述功能單元之間的交互的"S"和"D",分別表示 state 和 dispatch 方法,其中

  • dispatch:是組件觸發 Action 的唯一方法

熟悉 Redux 的人並不會對 Action 和 Reducer,以及 dispatch 的概念陌生,我們也能從上述職責描述中看出,不管是在 Redux 還是 React Hooks 中,這幾個功能單元的職責是相同的,React 引入這些功能單元,也是為了處理各種複雜狀態邏輯的場景。這裏,巧妙的使用了 Context API 進行全局狀態管理,使用 Hooks 提供的 useReducer 來去處理組件狀態的變化,既使得數據流向變得清晰、可預測,又避免 Redux 複雜的模板代碼生成和數據流管理。接下來,我們來通過一個例子看一下代碼的具體實現。

舉例

我們以遊客在頁面查看評論列表為例,來使用 React Context + Hooks 實現評論數據的全局狀態管理

創建 React Context

我們需要在 React App 中創建一個 context 對象來存儲評論列表數據。基於架構我們知道,React Context 通過 Context.Provider 來為消費組件提供存儲的 context,所以這裏我們先創建一個 <CommentProvider /> 來返回 Context.Provider 和 Context 對象本身。

// CommentProvider.js
const CommentContext = createContext({});

const CommentProvider = ({ children }) => { 
    const [state, dispatch] = useReducer(commentReducer, { comments: [] });
    return ( 
        <CommentContext.Provider value={{ state, dispatch }}> 
            {children} 
        </CommentContext.Provider> 
    );
};

<CommentProvider/> 中,調用 useReducer Hook 來去註冊 commentReducer(會在後面創建),將解構的 state 和 dispatch 函數通過 CommentContext.Provider 提供給子組件。

當前是將 state 和 dispatch 未經過加工直接提供給了 Provider,很多實現會在 Provider 中解構 state,只去傳遞 <CommentProvider/> 需要的 state,另外也會將組裝好的 Actions 直接傳遞給消費組件。雖然這種方式更明確要傳遞的數據和方法,但是解構 state 和 創建 Actions 的邏輯由於封裝在 Provider 中,導致很難測試。所以,這裏還是借鑑了 Redux 的方式,在獨立的 Action Creator 去定義 Action,然後在組件中直接調用。

通過 CommentProvider 來組裝 CommentList 組件

構建好 <CommentProvider/> 以後,在 <App/> 中,便可以通過 <CommentProvider/> 來組裝 <CommentList /> 了。

// App.js
const App = () => ( 
    <CommentProvider> 
        <CommentList /> 
    </CommentProvider>
);

接下來,我們來看一下,如何在 <CommentList /> 中去使用 Context.Provider 提供的 context 值。

創建 CommentList 組件

// CommentList.js
import { fetchComments } from '../commentAction';

function CommentList() {
    const { state, dispatch } = useContext(CommentContext);
    
    useEffect(() => { 
        fetchComments(dispatch); 
    }, []);
 
    return (<ul> 
        {state.comments.map((comment) => (
            <li key={comment.id}><p>{comment.body}</p><span>{comment.name}</span></li> 
        ))}
    </ul>);
}

熟悉 React 的話應該清楚,通常在 useEffect Hook 中去處理 API 請求,來獲取評論列表數據。在未使用 Context 對數據進行管理時,在組件內會直接通過組件內 state 來存取列表數據,然後觸發組件的重新渲染。這裏不同的是,我們通過 useContext 獲取了全局的 state 數據,然後在 useEffect Hook 中,調用了 fetchComments Action,並沒有直接在組件內調用 setState。我們在架構圖中跟蹤 "S" 的變化,可以看到,調用 Action 後,Reducer 會對 state 進行更新,並將新的 state 值更新到 context 中。那我們就來看一下,如何創建 Action 和 Reducer 來更新 state 中評論列表的值。

構建 commentAction

// commentAction.js
const fetchComments = async (dispatch) => { 
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1/comments');
    dispatch({ type: 'SET_COMMENTS', payload: response.data });
}

當異步獲取評論列表數據後,便通過傳入的 dispatch 方法來去發出一個更新評論列表的 'SET_COMMENTS' 的 Action,commentReducer 在接收到這個 Action 以後,便會從
payload 中取出傳遞的評論列表數據,然後更新到 context 的全局狀態中。

構建 commentReducer

// commentReducer.js
const commentReducer = (state, action) => { 
    if (action.type === 'SET_COMMENTS') { 
        return { ...state, comments: action.payload }; 
    }
    return state;
};

需要注意的是,和 Redux 強調的一致,reducer 是一個純函數,只根據傳入的 Action 和 舊的 state,返回一個新的 state 值。context 拿到新的 state 後,便會對舊的 state 進行替換。到此,一個完整的更新評論列表的數據流已經走完。

總結

我們借用 React 引入的 Context 來去進行狀態管理,通過 useReducer Hook 來去更新 state 的變化,並且借鑑了 Redux 中 Action 的抽象,來去描述組件中發生的事件或者變化。這種方式對全局狀態的讀取和更新進行了隔離,使得數據在功能單元間的流向更加清晰、易維護。

在實際項目中,需要注意 Context.Provider 中的嵌套關係,需要在合適的父節點提供 context 的值。可以通過UI或業務組件和狀態之間的業務關係來去梳理。

可以在 https://github.com/dujuanxian/react-context-with-hook-comments-demo 中查看代碼的實現,其中除了查看評論列表的功能,還包含創建評論的功能。

更多文章,請查看我的博客 dujuan.in

Add a new Comments

Some HTML is okay.