用useContext, useReducer 做類似Redux的global state(原理篇)
不用redux也可以做global state
索引index
Context Store: 負責global state 管理和Provider
Actions: 負責發送「行動指令和資料」
Reducers: 負責處理從action得到的「行動指令和資料」並傳給state
Context Store
基本介紹
負責global state 管理,會接收reducer更新後的state傳給component做渲染,因為react中只有useContext這個API,一般可以在組件中直接使用此hook取得store中的值,像是以下的範例
// ...
const Button = () => {
const { state } = useContext(ContextStore); //...
}
不過今天我們參考redux的做法,將可重用的部分整理成三個function,整合useContext和useReducer的hook,方便我們在寫WebApp時可以直接使用ContextStore
三個function分別為createContextStore, createContextValueFn, ContextWrapperFn
createContextStore()
function createContextStore<State extends object>(initState: State, dispatch?: Dispatch): Context<MyContext<State>>
負責產生Store的function,主要是使用react 的 createContext()的API,儲存我們的global state
createContextValueFn(initState, reducers)
function createContextValueFn<State extends Obj>(initState: State, reducers: Reducers<State>): (customInitState?: Partial<State> | undefined, customDispatch?: Dispatch | undefined) => {
dispatch: Dispatch;
state: State;
}
接收 Reducers和state的初始值,產生一個contextValue 的 “function”,並提供customInitState和customInitDispatch的接口
useReducer置於此function中,此function主要是給ContextWrapperFn使用
ContextWrapperFn
function ContextWrapperFn<ContextState>(ContextValueFn: ContextValueFnType<ContextState>, ContextStore: React.Context<{
dispatch: (x?: any) => any;
state: ContextState;
}>): (props: ContextWrapperProps<ContextState>) => JSX.Element
最後整合以上的functions,產生一個ContextWrapper,提供給各個component使用,在使用Store之前需要先在component外圍“包裹起來”才能取得Store的值
const ContextWrapper = ContextWrapperFn(ContextValueFn, ContextStore)const ButtonWithCtx = () => (
<ContextWrapper><Button /></ContextWrapper>
)
Actions
基本介紹
負責發送type 和 payload給Reducers,雖說actions一般都是以下列範例的格式呈現,但其實不限定寫法,只要reducers能夠正確接收actions即可
建議同一個store中的state都用同一種action寫法以免混亂
const addNumber = ({ number }) => ({
type: "ADD_ONE",
payload: number,
})// 也可以這樣寫
const addNumber = ({ number }) => ({
actionType: "ADD_ONE",
payload: { number, }
})
actions範例
// store state
export interface State {
count: number
}const initState: State = {
count: 0,
}
actions/index.ts (負責該state的所有actions管理)
import { CountActions } from "./count-actions."const ActionTypes = {
ADD_NUMBER: "ADD_NUMBER",
MINUS_ONE: "MINUS_ONE",
}export type Actions = CountActionsexport default ActionTypes
actions/count-actions.ts
import ActionTypes from "./index"export interface AddNumberActionPayload { countNumber: number }export interface AddNumberAction {
type: ActionTypes.ADD_NUMBER
payload: AddNumberActionPayload
}
export interface MinusOneAction {
type: ActionTypes.MINUS_ONE
}export const addNumber = (countNumber: number): AddNumberAction => ({
type: ActionTypes.ADD_NUMBER,
payload: { countNumber },
})
export const minusOne = (): MinusOneAction => ({
type: ActionTypes.MINUS_ONE,
})export type CountActions = AddNumberAction | MinusOneAction
component可以使用 addNumber和 minusOne這兩個functions,搭配dispatch發送actions給reducer,而關於component的詳細用法會在應用篇詳細解釋
Reducers
基本介紹
Reducers就像是資料處理中心,輸入指令和資料,輸出最新的值,因此Reducers會是一個Pure Function,不會更改原本store的值,只會給你一個全新的store/state值
實際上運作流程大致上為:Reducers接收actions的type & payload以及store,處理完後將最新的store值丟給store去更新,store的值更新後觸發component的更新渲染
reducers範例(承接actions部分)
reducers/index.ts
此部分的reducers就是提供給createContextValueFn使用,範例中的combineReducers會在應用篇詳細解釋
import count from "./count-reducers"const reducers = combineReducers({ count: count, })
reducers/count-reducers.ts
import ActionTypes, { Actions } from "actions/index" // actionsindex.ts的位置function count(state: State, action: Actions): State["count"] {
switch(action.type) {
case ActionTypes.ADD_NUMBER:
return state.count + action.payload.countNumber;
case ActionTypes.MINUS_ONE:
return state.count - 1;
default:
return state.count
}
}export default count
以上三大要素(Store, Actions, Reducers)完成,我們就可以應用在component了,至於如何與component做結合和實作,會在應用篇詳細解釋。
如果有用過redux,對以下幾個function應該會很眼熟,應用篇會介紹這些function,並應用在component。
mapStateToProps, mapDispatchToProps, connectCtx, combineReducers