React Hook 实战指南!(3)

实现React Hook版的Store

前言

众所周知,由于React自己只是View层的框架,对于总体业务架构来讲是有缺失的,因此咱们常常会在React应用中接入Flux、Redux等架构模式,固然也能够选择使用Mobx(相似Vuex)等集成工具。javascript

就拿使用较广的Redux架构来讲,在React中实现后,每每须要将store中的数据挂载到组件的状态中,当subscribe到state改变后再调用setState来更新组件对应的状态来实现数据同步。好的,问题来了,使用Hook后组件内部该怎么处理呢?其实很简单,只须要利用useState来构建状态就行了。vue

const Example = () => {
  const [count, setCount] = useState(store.getState().count)
  store.subscribe(() => {
  	setCount(store.getState().count)
  })
	return (<div>{count}</div>)
}
复制代码

是否是很简单呢?固然了,咱们通常会使用react-redux来简化redux的使用,使用来Hook后,对应的工具库固然也要作更换,增长一大波学习成本。java


useReducer的使用

咱们要实现的Todolist实例组件间其实有很强的联动性,因此必然要将一些数据进行集中的管理。react

其实React 提供了不少Hook工具,不仅有咱们看到的useState,其余的咱们慢慢来学习,咱们先来学习一下useReducer的使用,这个东西就能够帮助咱们构建一个简版的Store,虽说是简陋来一些,可是咱们构建的合理一些其实也能知足应用的需求。vuex

const [state, dispatch] = useReducer(reducer, initialArg, init);
复制代码

useReducer实际上是useState的升级版本,能够利用一个(state, action) => newState 的 reducer来构建数据,而且还能够生成与之匹配的dispatch方法,你们看到这些就忽然发现,这个玩意和redux很像对吧,哈哈,ok,那咱们再来看一下这个东西怎么用。redux

// 初始状态
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方法给内部的子组件传入。ide

可是,这样依然存在一个问题待解决:一层一层的传数据太麻烦了,不科学。那该怎么解决呢?

没错,聪明的小伙伴已经想到了,咱们用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
}
复制代码

上面的代码构建了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的使用。