前言
作为一名合格的前端童鞋,应该对redux、vuex之类的状态管理插件耳熟能详。自react16.8引入hooks特性后,博主一直有利用react自身特性实现redux的想法!本文主要探究如何利用react自身特性去实现redux相关功能,欢迎点评!
正文
1 redux解决了什么问题
我想很多人关于这个问题的答案是:状态管理、数据视图分离。也对,redux的确解决了状态管理的问题,而且做到了数据视图分离。那么,将数据和视图分离究竟是为了什么?我想会有许多人答便于维护、规范化云云,关于这些答案,个人认为,数据视图分离大多数场景会带来更多的维护上的开销,而且团队没有同一编写规范的话,代码只会显得更加凌乱不堪!实际上,分离数据视图的主要动机是为了作数据/状态共享!再看下官方对redux的定义:Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。细想一下,官方定义这种state特性其实vux/react内部已经具备了的,比如vue或react都支持组件状态以及通过props控制子组件状态。所以说,redux的核心还是在于数据/状态共享,可以让组件之间共享状态,避免为了共享状态而持续状态提升。
而react context也恰好具备作数据/状态共享的特性,看下context的官方定义:Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
2 利用context实现redux功能
2.1 基本思路以及带来的两个问题
将context值托管给外部hooks管理(contextHook),并将context provider注册到应用根组件上,当消费者通过消费行为更改contextHook状态时, context provider和context consumer对应组件会更新。但是这样做会带来以下问题:
(1)当更改contextHook状态时,context provider组件直接子组件也会更新,导致额外的组件更新;
(2)使用多个context,需要显式在根组件之上反复注册,状态都声明在一个文件中,比较麻烦,类似:
const [value1, dispatch1] = useReducer(reducer, {})
const [value2, dispatch2] = useReducer(reducer, {})
<Context1.Provider value={{vlaue1, dispatch1}}>
<Context2.Provider value={{vlaue2, dispatch2}}>
<div>Root</div>
</Context2.Provider>
</Context1.Provider>
2.2 问题一解决思路
可以使用React.memo和高阶组件开启组件记忆功能,示例如下:
// 根组件
const Root = memo((props) => <div>{{props.name}}</div>)
const wrapRoot = () => {
return (props) => {
const [value1, dispatch1] = useReducer(reducer, {})
return (
<Context1.Provider value={{vlaue1, dispatch1}}>
<Root {...props}/>
</Context1.Provider>
)
}
}
2.3 问题二解决思路
利用高阶组件特性进行抽象化封装,示例如下:
const wrapStore = (Comp: React.ComponentType, contextName: string) => {
return memo((props: any) => { //这里用memo可以防止context provider之间相互影响
const initial = {}
const context = React.createContext(contextName, initial)
const [value, dispatch] = useReducer(reducer, initial)
const Child = memo(Comp)
return (
<Context.Provider value={value}>
<Child {...props} />
</Context.Provider>
)
})
}
const Index = wrapStore(wrapStore(()=> <div>Root</div>,'store1'), 'store2')
// app.tsx
<Index/>
3 模块化方案
鉴于redux有模块化方案,而且的确对开发带来了便利,因此加入模块化功能!下面讲解基本思路: 第一步,利用闭包将需要用作状态管理的context集中管理起来,context的获取必须通过闭包。示例如下:
export const contextFactory = (() => {
const contextObj: { [module: string]: React.Context<any> } = {}
return {
getContext: (module: string) => {
if (!(module in contextObj)) {
contextObj[module] = React.createContext({})
}
return contextObj[module]
},
}
})()
第二步,将contextHook、模块名、初始值以及reducer用特殊数据结构做声明,然后存储到context闭包作用域下,便于后续获取context相关信息。
export interface StoreType<T> {
name: string
entryHook: Function
initial: T
reducers?: {
[key: string]: (state: any, payload: any) => any
}
}
export const contextFactory = (() => {
const contextObj: { [module: string]: React.Context<any> } = {}
const contextDataObj: { [key: string]: StoreType<any> } = {}
return {
getContextData(name: string) {
return contextDataObj[name]
},
getContextDataObj() {
return contextDataObj
},
registerContextData(store: StoreType<any>) {
if (Object.values(contextDataObj).includes(store)) return store
if (Object.keys(contextDataObj).includes(store.name))
throw Error(`store ${store.name} has existed!`)
contextDataObj[store.name] = store
return store
},
getContext: (module: string) => {
if (!(module in contextObj)) {
contextObj[module] = React.createContext({})
}
return contextObj[module]
},
}
})()
第三步,改造高阶函数,应用上第二部声明的闭包
const wrapStore = (Comp: React.ComponentType, name: string) => {
const Context = contextFactory.getContext(name)
const store = contextFactory.getContextData(name)
return memo((props: any) => {
console.log(`withContext ${name} rerendered`)
const state = store.entryHook()
return (
<Context.Provider value={state}>
<Comp {...props} />
</Context.Provider>
)
})
}
export const withContext = (
EntryComp: React.ComponentType,
storeObj: { [key: string]: StoreType<any> }
) => {
if (
!Object.keys(storeObj).length ||
!Object.values(storeObj).filter(Boolean).length
)
return EntryComp
Object.values(storeObj).forEach((store) => {
contextFactory.registerContextData(store)
})
let Comp: React.ComponentType = memo(EntryComp)
const names = Object.keys(contextFactory.getContextDataObj())
return names.reduce((AccComp, curName) => wrapStore(AccComp, curName), Comp)
}
最后,使用给定的withContext高阶函数
const store1 = {
name: 'test1',
entryHook: useTestStore1,
initial : {
count1: 20,
},
reducers: {
add(state) {
return {...state, count1: state.count1 + 1}
}
},
}
function useTestStore1() {
const {reducer, initial} = yxl_rhs.reducerProvider(store)
const [state, dispatch] = useReducer(reducer, initial)
return {
state,
dispatch,
}
}
const store2 = {
name: 'test2',
entryHook: useTestStore2,
initial : {
count2: 20,
},
reducers: {
add(state) {
return {...state, count2: state.count2 + 1}
}
},
}
function useTestStore2() {
const {reducer, initial} = yxl_rhs.reducerProvider(store)
const [state, dispatch] = useReducer(reducer, initial)
return {
state,
dispatch,
}
}
const Index = () => <>hello,world</>
const App = withContext(Index, {store1, store2})
提示:推荐在hooks中获取闭包作用域内的context完成订阅
4 库文件
上述方案已被作者封装成npm库'yxlolxy-open-api',欢迎各位安装试用!