1. 首页

React Hook 实战指南!(3)

实现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需要有这样的条件:

  1. 首先,它得是个函数
  2. 其次,它得返回点东西给组件们使用

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

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React Hook 实战指南!(3)

链接:https://www.javascriptc.com/3829.html

« 撸一个前端监控系统(React + Node + Mysql + Webpack plugin + Docker)—— (上)
前端架构性能优化策略»
Flutter 中文教程资源

相关推荐

QR code