透過 React CreateContext 搭配 React useContext 與 useReducer 來做 Global State Manager

Whien
13 min readMar 1, 2019

服用 CodeSandbox 範例

Code Sandbox:https://codesandbox.io/s/0o10kq73zv

碎碎念

Hooss 改善以往依賴 Class Instance 的問題,再來就是透過 Hooks 解決問題。

老規局,先來一首Owl City 的歌

Owl City — Shooting Star [Official Lyric Video]

為什麼需要 Global State?解決什麼問題?

已經有 state 的元件為什麼需要 Global State 呢?Global State 的出現是因為當有階層較多的元件時,可能一個狀態需要透過 3–5 層不等的資料流才能獲取,也因此 Global State 可以直接透過 Context 來獲取及給予狀態,Global State 顧名思義,說穿了就是一個產生在最上層的全域變數。

事前預備知識

  • React Redux
  • React Context API
  • React useContext
  • React useReducer

大綱

  • React Redux 快速簡介
  • 使用 React Context API
  • 使用 React useContext
  • 使用 React useReducer
  • 組合並完成 Global State Manager

React Redux 快速簡介

在 React 尚未納入 useReducer 之前,必須依賴的是 redux 與 react-redux 的搭配來將 Global State 導入 React 中,在以往都必須要使用 Higher Order Component 的技巧來使用。

簡單範例

import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
//...reducers
import reducers from './reducers';
const store = createStore(reducers);function Application() {
return (
<Provider store={store}>
<Todos />
</Provider>
)
}
function Todos() {
return <div>My todo list</div>
}
const mapStateToProps = state => ({})
const mapDispatchToProps = {}
const ConnectTodos = connect(
mapStateToProps,
mapDispatchToProps
)(Todos);

大致上,這樣就可以將 Todos 透過 connect 接到 Provider 的 context 來獲取狀態。

不過不過,這是必須透過 redux 與 react-redux 這兩項套件來達成,而 Hooks 的出現可以完全讓這兩個套件消失無影蹤,接下來就一起來探索如何透過 Hooks 來達到此效果。

使用 React Context API

稍微了解一下 React Context API 的運作

首先要先知道幾個東西

  • React.createContext
  • Context.Provider
  • Context.Consumer
// 建立一個 Context
const ContextStore = React.createContext({
todos: []
})
// 使用 ContextStore
function Application() {
return (
<ContextStore.Provider value={{todos: ['run']}}>
<Todos />
</ContextStore.Provider>
)
}
// Todos
function Todos() {
return (
<ContextStore.Consumer>
{value => value.todos.map(todo => <div key={todo}>todo</div>)}
</ContextStore.Consumer>
)
}
結果

透過以上範例,我們可以知道 Context Provider 負責透過 value 來改變狀態,而 Context Consumer 來負責傳入 value 狀態。

使用 React useContext

規格:useContext(context: React.createContext)

每次都要使用 Context Consumer 來獲取 value 狀態實在很綁手綁腳,於是 hooks 中 useContext 的出現幫助了開發上的便利,只要將範例的內容稍微更改一下,就能得到相同結果了。

// 建立一個 Context
const ContextStore = React.createContext({
todos: []
})
// 使用 ContextStore
function Application() {
return (
<ContextStore.Provider value={{todos: ['run']}}>
<Todos />
</ContextStore.Provider>
)
}
// Todos
function Todos() {
const value = React.useContext(ContextStore)

return (
<React.Fragment>
{
value.todos.map(todo => <div key={todo}>{todo}</div>)
}
</React.Fragment>
)
}

有沒有發現 useContext 的好處,再也不用透過 render props 來得到 value 內容了,程式內容看起來也更直觀了。

使用 React useReducer

規格:useReducer(reducer, initialState)

  • reducer 自訂 reducer action 內容
  • initialState 自訂 state 初始內容

接下來 useReducer 我想是因為 redux 的出現後,大家已經開始能習慣使用 action, reduce 的方式來改變狀態,於是 React 就將此方式直接納入了 React Core 中使用,是一個很棒的加入,使用方式就跟以往在建立 redux reducer 一樣。

// 建立 todos initial state
const todosInitialState = {
todos: []
}
// 建立 reducer
function todosReducer(state, action) {
switch(action.type) {
case 'ADD_TODO':
return Object.assign({}, state, {
todos: state.todos.concat('eat')
})
default:
return state
}
}
// 使用 reducer hook
function Todos() {
const [state, dispatch] = React.useReducer(todosReducer, todosInitialState)
function handleClick() {
dispatch({type: 'ADD_TODO'})
}
return (
<React.Fragment>
{
state.todos.map((todo, index) => <div key={index}>{todo}</div>)
}
<button onClick={handleClick}>ADD TODO</button>
</React.Fragment>
)
}

以上範例即可完成透過 dispatch 來改變狀態的 redux 行為,就此我們已經可以將 redux 移除了。

組合並完成 Global State Manager

規格:createContext + useContext + useReducer

大致上了解三種各自的作用以後,就可以來嘗試組合這些內容,然後做一個 Global State Manager

達成目標:

  • products store
  • orders store

為了方便我們要完成幾個目標

  • reducer 的 state 要綁在一起
  • dispatch 能夠隨時被呼叫

reducer 的 state 要綁在一起

首先為了將 state 綁在一起,需要先建立一個 Global State Context

const productsInitState = { products: [] }
const ordersInitState = { orders: [] }
const ContextStore = React.createContext({
products: [],
orders: []
})

建立 Products reducer

function productsReducer(state, action) {
switch(action.type) {
case 'ADD_PRODUCT':
return Object.assign({}, state, {
products: state.products.concat({ id: state.products.length })
})
default:
return state
}
}

建立 Orders reducer

function ordersReducer(state, action) {
switch(action.type) {
case 'ADD_ORDER':
return Object.assign({}, state, {
orders: state.orders.concat({ id: state.orders.length })
})
default:
return state
}
}

將 Reducer 傳入 Context 中

從範例中可以發現,Context 不僅僅可以傳入 State,就連 Function 都可以傳入,思考不要被綁住了

function Application() {
const [pState, pDispatch] = React.useReducer(productsReducer, productsInitState);
const [oState, oDispatch] = React.useReducer(ordersReducer, ordersInitState);

return (
<ContextStore.Provider
value={{
orders: oState.orders,
products: pState.products,
pDispatch,
oDispatch
}}
>
<MyApp />
</ContextStore.Provider>
)
}
function MyApp() {
return (
<React.Fragment>
<Products />
<Orders />
</React.Fragment>
)
}

建立 Products Component 並取用 ContextStore

function Products() {
const { products, pDispatch } = React.useContext(ContextStore);
return (
<div>
{
products.map(product => (<div>PRODUCT - {product.id}</div>))
}
<button onClick={() => pDispatch({type: 'ADD_PRODUCT'})}>ADD PRODUCT</button>
</div>
)
}

建立 Orders Component 並取用 ContextStore

function Orders() {
const { orders, oDispatch } = React.useContext(ContextStore);
return (
<div>
{
orders.map(order => (<div>PRODUCT - {order.id}</div>))
}
<button onClick={() => oDispatch({type: 'ADD_ORDER'})}>ADD ORDER</button>
</div>
)
}

如此就能開始不斷增加 reducer 來達到 Global State 的管理。

奇怪技巧

假如有個奇怪的需求需要把 dispatch 綁在一起可以這麼做,建立一個 combineDispatch 方法

function combineDispatchs(dispatchs) {
return function(obj) {
for (let i = 0; i < dispatchs.length; i++) {
dispatchs[i](obj)
}
}
}

完成方法後傳入 Dispatchs 並改變 Context value 內容

<ContextStore.Provider
value={{
orders: oState.orders,
products: pState.products,
dispatch: combineDispatchs([pDispatch, oDispatch])
}}
>
<MyApp />
</ContextStore.Provider>

如此一來假如要改變 Orders 就可以直接使用 dispatch 就好了。

function Orders() {
const { orders, diaptch } = React.useContext(ContextStore);
return (
<div>
{
orders.map(order => (<div>PRODUCT - {order.id}</div>))
}
<button onClick={() => diaptch({type: 'ADD_ORDER'})}>ADD ORDER</button>
</div>
)
}

完整範例

Code Sandbox:https://codesandbox.io/s/0o10kq73zv

講在結尾

最近自己使用 Hooks 的心得就是,有些操作上還是無法完全透過 Hooks 來完成,Class Instance 的配合其實還是必須的,Hooks 只是一個替代方案,有需要時還是會一起使用,達到需求。

我是懷恩
目前是一位
YOSGO 的核心開發者
LetFreeCode 創辦人
在一個我熱愛的新創團隊與事物活躍著
是個全職的開發者,我熱愛開發與分享
「前端專注在瀏覽器的相關開發、效能與互動體驗」
「後端專注在網頁伺服器的效能調適與架構的配置」
「對於網頁技術有著熱衷,隨時跟在新技術的旁邊」
如果您有任何問題,歡迎與我聯繫,我也時常在自己 FB 上開直播做紀錄
Github: https://github.com/madeinfree
FB:https://www.facebook.com/haowei.liou
Email: sal95610@gmail.com

最近(外加休閒安排)已經時間滿到沒辦法執行太多活動,但還是很歡迎大家與我一起討論。

--

--

Whien
Whien

Written by Whien

遨遊在硬體與軟體世界中,對於計算機一切事物都充滿好奇及熱情。