实现React Hook版的Store
前言
众所周知,因为React本身只是View层的框架,对于整体业务架构来说是有缺失的,所以我们经常会在React应用中接入Flux、Redux等架构模式,当然也可以选择使用Mobx(类似Vuex)等集成工具。
就拿使用较广的Redux架构来说,在React中实现后,往往需要将store中的数据挂载到组件的状态中,当subscribe到state改变后再调用setState来更新组件对应的状态来实现数据同步。好的,问题来了,使用Hook后组件内部该怎么处理呢?其实很简单,只需要利用useState来构建状态就好了。
const Example = () => {
const [count, setCount] = useState(store.getState().count)
store.subscribe(() => {
setCount(store.getState().count)
})
return (<div>{count}</div>)
}
是不是很简单呢?当然了,我们一般会使用react-redux来简化redux的使用,使用来Hook后,对应的工具库当然也要做更换,增加一大波学习成本。
useReducer的使用
我们要实现的Todolist实例组件间其实有很强的联动性,所以必然要将一些数据进行集中的管理。
其实React 提供了很多Hook工具,不只有我们看到的useState,其他的我们慢慢来学习,我们先来学习一下useReducer的使用,这个东西就可以帮助我们构建一个简版的Store,虽然说是简陋来一些,但是我们构建的合理一些其实也能满足应用的需求。
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer其实是useState的升级版本,可以利用一个(state, action) => newState
的 reducer来构建数据,并且还可以生成与之匹配的dispatch方法,大家看到这些就突然发现,这个玩意和redux很像对吧,哈哈,ok,那我们再来看一下这个东西怎么用。
// 初始状态
const initialState = {count: 0};
// reducer纯函数,接收当前的state和action,action身上带有标示性的type
// reducer每次执行都会返回一个新的数据
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
// 利用useReducer生成状态
// 第一个参数为处理数据的reducer,第二个参数为初始状态
// dispatch每次调用都会触发reducer来生成新的状态,具体的处理由传入的action决定
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
useReducer可以接收三个参数。第一个参数为处理数据的reducer,第二个参数为初始状态,第三个参数可以不传,它的用处是惰性处理初始状态,例如:
// 初始状态
const initialState = 0
function reducer(state, action) {
// ...
}
function init (state) {
return { count: state }
}
function Counter() {
// 虽然我们传入的初始状态是 0 ,但是最终的初始化状态为: init(initialState)
// 所以最终初始状态为{ count: 0 }
// 其实用处不大...脱裤子放屁的感觉....
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
// ...
);
}
ok,这不state也有了,reducer也有了,根据dispatch咱们再整个actionCreator 出来,小小的Redux架构不就搭建完成了么?
在动手之前,我们想到一个问题,useReducer是要在函数组件里面使用的呀,我们不能在每一个要使用state的组件中都利用useReducer构建一个store吧?那样不就各自为政了嘛,还谈什么状态集中管理,那怎么办呢?
答案就是:整出一个大家的爸爸(最外层的父组件)来,然后套在外面,由这个父组件来管理这些状态,然后再将状态和action方法给内部的子组件传入。
但是,这样依然存在一个问题待解决:一层一层的传数据太麻烦了,不科学。那该怎么解决呢?
没错,聪明的小伙伴已经想到了,我们用context来解决数据的传递问题,那么利用context传递有什么不好的地方吗?
答案是没啥事儿,react-redux是怎么让所有的容器组件都能获取到store中的状态再传递给UI组件的呢,还不是在最外面有个Provider利用context树给它们提供了store嘛!
ok,一切问题都解决了,准备开始!
实现Store
构建store的方法其实很简单,但是为了结构分离顺便再多搞一个知识点,我们准备利用一个自定义Hook来完成store的构建。
自定义Hook构建store
自定义Hook的目的是将一些逻辑提升到某个可重用的函数中,类似于HOC的存在一样,自定义的Hook需要有这样的条件:
- 首先,它得是个函数
- 其次,它得返回点东西给组件们使用
OK,心里有数了,我们先把其他的东西构建出来(actions, initialState):
默认状态,咱们利用immutable来构建不可变状态,优化性能:
import { is, fromJS } from 'immutable'
// fromJS可以将一个普通结构的数据生成为immutable结构的数据
const initialState = fromJS({
items: []
})
reducer,在内部已经实现了关于Todolist业务的一些处理,我们准备将todolist数据存放在localStorage中,为了操作方便使用localstorage包:
import LocalStorage from 'localstorage'
// 这个东东可以方便的操作localStorage
const TodoList_LS = new LocalStorage('todolist_')
// reducer接受当前的状态(设置默认状态)以及action
// action中包含此次动作的type标示与payload载体
const reducer = (state = initialState, { type, payload }) => {
// 准备返回的状态
let result = state
switch (type) {
// 更新全部items
case 'UPDATE_ITEMS':
// immutable基本操作,设置items后返回的就是一个新的状态
// 此时result !== state 哟
result = state.set('items', fromJS(payload.items))
break
// 新建某个item
case 'CREATE_ITEM':
result = state.set('items', state.get('items').push(fromJS(payload.item)))
break
// 完成某个item
case 'FINISH_ITEM':
result = state.set('items', state.get('items').update(
state.get('items').findIndex(function(item) {
return is(item.get('id'), payload.id)
}), function(item) {
return item.set('finished', !item.get('finished'))
})
)
break
// 更新item的title和description
case 'UPDATE_ITEM':
result = state.set('items', state.get('items').update(
state.get('items').findIndex(function(item) {
return is(item.get('id'), payload.item.id)
}), function(item) {
item = item.set('title', payload.item.title)
item = item.set('description', payload.item.description)
return item
})
)
break
// 删除某个item
case 'DELETE_ITEM':
let list = state.get('items')
let index = list.findIndex((item) => is(item.get('id'), payload.id))
result = state.set('items', list.remove(index))
break
default: break
}
// 将更新后的items存入localStorage中
TodoList_LS.put('items', result.get('items').toJS())
return result
}
在这里简单说一下immutable的使用,当数据转换为immutable数据后,利用对应的set、get、update等APi操作数据后都能返回一个新的数据。
大家可以看到reducer的操作基本与redux的reducer构建方式一样,内部包含的也仅仅是一些增删改差的简单操作。
接下来我们再来创造一个actions工具,内含很多方法,每个方法都可以调用dispatch来触发reducer的执行并传入对应的action(包含标识的type和数据载体payload)。
const actions = {
getInitialItems () {
let [err, items] = TodoList_LS.get('items')
if (err) items = []
this.dispatch({
type: 'UPDATE_ITEMS',
payload: { items }
})
},
createTodoItem ({ item }) {
let [err, id] = TodoList_LS.get('id')
if (err) id = 0
item.id = ++id
item.finished = false
this.dispatch({
type: 'CREATE_ITEM',
payload: { item }
})
TodoList_LS.put('id', item.id)
},
finishTodo ({ id }) {
this.dispatch({
type: 'FINISH_ITEM',
payload: { id }
})
},
deleteTodo ({ id }) {
this.dispatch({
type: 'DELETE_ITEM',
payload: { id }
})
},
updateTodoItem ({ item }) {
this.dispatch({
type: 'UPDATE_ITEM',
payload: { item }
})
}
}
大家可以看到在actions的方法中都在调用一个this.dispatch方法,这个方法是哪来的呢,我们一会儿就把useReducer生成出来的reducer挂载到actions身上不就有了么。
最后轮到我们的自定义Hook了,甩出来瞅瞅:
// 构建Store的Custom Hook
const StoreHook = ( ) => {
// 利用useReducer构建state与dispatch
let [ state, dispatch ] = useReducer(reducer, initialState)
// 为actions挂载dispatch,防止更新的时候挂载多次
if (!actions.dispatch) actions.dispatch = dispatch
// immutable数据转换为普通结构数据
let _state = state.toJS()
// Hook生成的数据
let result = [
_state,
actions
]
return result
}
我们构建的自定义Hook-StoreHook在实例中没有复用的场景,在这里仅仅是为了分离Store的构建以及学习自定义Hook。
在StoreHook中利用useReucer生成了state和dispatch方法,将dispatch方法挂载在actions身上以便actions内部的方法来调用触发reducer,将生成的状态及actions返回出去,这样使用StoreHook的组件就可以得到我们构建好准备集中管理的state和actions了。
let [state, actions] = StoreHook()
利用Context及Custom Hook来使用state & actions
上面我们以及讨论过了,只要使用我们自定义的StoreHook,就可以得到state和actions,但是整个实例只能集中的管理一个state,所以我们不能在多个组件中同时使用StoreHook,所以我们需要构建一个专门用来构建state和actions并将其传递给所有子组件的“大家的爸爸”组件。
export const StoreContext = React.createContext({})
export const HookStoreProvider = (props) => {
let [state, actions] = StoreHook()
return (
<StoreContext.Provider value = {{ state, actions }}>
{ props.children }
</StoreContext.Provider>
)
}
大家可以看到HookStoreProvider组件在构建了context将state和actions进行传递,非常棒,把它包在组件结构的最外面吧。
import { HookStoreProvider } from '@/hooks/todolist'
class App extends Component {
render () {
return (
<HookStoreProvider>
<TodoList/>
</HookStoreProvider>
)
}
}
export default App
ok,那么我们的组件需要怎么去使用HookStoreProvider在context树上传递的状态和组件呢?传统的context使用方法倒也可以:
import { StoreContext } from './store.js'
const TodoListContent = () => {
return (
<StoreContext.Consumer>
{ (value) => (<div>{ value }</div>) }
</StoreContext.Consumer>
)
}
这样的使用方式有点麻烦,幸好React Hook提供来useContext的Hook工具,这就简单多了:
let values = useContext(StoreContext)
let { state, actions } = values
useContext传入Context对象后可以返回此Context对象挂载在context树上的数据,这就不需要什么Consumer了,简单粗暴,那么我们在大胆一点,为了让想要使用状态和actions的组件连useContext都不用写,而且还可以通过传个getter参数就能去到state中的某个状态的衍生状态,(类似于vuex中的getters),这样的“中间商”正好可以再利用Custom Hook(自定义Hook)来构建:
// 关于state的衍生状态
const getters = {
// 关于todos的展示信息数据
todoDetail: (state) => {
let items = state.items
let todoFinishedItems = items.filter(item => item.finished)
let todoUnfinishedItems = items.filter(item => !item.finished)
let description = `当前共有 ${items.length} 条待办事项,其中${todoFinishedItems.length}条已完成,${todoUnfinishedItems.length}条待完成。`
// 返回描述、已完成、未完成、全部的items
return {
description,
todoItems: items,
todoFinishedItems,
todoUnfinishedItems
}
},
// 返回所有的items
items: (state) => {
return state.items
}
}
// 自定义Hook,接受context中的状态并根据传入的getter来获取对应的getter衍生数据
export const useTodolistStoreContext = ( getter ) => {
let {state, actions} = useContext(StoreContext)
let result = [
getter ? getters[getter](state) : state,
actions
]
return result
}
Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到
上面的代码构建了getters工具和useTodolistStoreContext自定义Hook,这样只要在组件内使用这个Hook就可以得到state或者衍生的getters以及actions了。
const TodoListContent = (props) => {
let [items] = useTodolistStoreContext('items')
return (
<div className="todolist-content">{ items }</div>
)
}
后语
这样我们的关于Store的构建也以及完成了,在这里我们研究了useReducer、useContext以及自定义Hook的使用了。
后面的内容中,我们将在实例的继续构建中学习useState,useEffect,useRef的使用。
作者:半盏屠苏
链接:https://juejin.im/post/5d78b085f265da03e83b983b
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com