手写一个简易的Hooks风格多模块Redux

665 阅读3分钟

在用React开发项目时,多多少少需要状态管理,各种状态管理库层出不穷。但有时候一个项目不需要太重的状态管理库。React 16.8后推出了Hooks写法,我们可以利用这一个新特性,手写一个Hooks风格的简易多模块Redux。

默认已经熟悉各种Hooks的用法

1 使用Hooks创建状态上下文

# src/redux/index.js

import React from 'react'

const StateContext = React.createContext()
const DispatchContext = React.createContext()

export const Provider = ({ children, store }) => {
  const [state, dispatch] = React.useReducer(store.reducers, store.state)

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}

export const useRedux = () => {
  const state = React.useContext(StateContext)
  const dispatch = React.useContext(DispatchContext)

  if (state === void 0 || dispatch === void 0) {
    throw new Error('useRedux must be used whithin a Provider')
  }

  return [state, dispatch]
}


  1. 我们创建了StateContextDispatchContext用于管理state和dispatch的状态。当然这里可以直接创建单独的storeContext进行管理。这样分开定义有助于理解。
  2. 定义Provider方法,接收两个参数childrenstore,用于接收传递的子组件和store状态。
  3. 利用useReducer的特性,可以获得类似redux的state和dispatch。
  4. 分别将statedispatch赋值给由StateContextDispatchContext提供的Provider,这样statedispatch将会在整个节点树上传递下去。
  5. 自定义HooksuseRedux,将定义好的statedispatch返回出去。

2 如何实现多模块的Redux

在项目的src/store中分别创建index.jscounter.jsuser.js

# /src/store/user.js

const UserModule = {
  state: {
    token: ''
  },
  reducer: (state, action) => {
    switch (action.type) {
      case 'SET_TOKEN':
        return { ...state, token: 'token-string' }
      case 'RESET_TOKEN':
        return { ...state, token: '' }
      default:
        return state
    }
  }
}

export default UserModule
# /src/store/counter.js

const CounterModule = {
  state: {
    count: 0,
  },
  reducer: (state, action) => {
    switch (action.type) {
      case 'increment':
        return { ...state, count: state.count + 1 }
      case 'decrement':
        return { ...state, count: state.count - 1 }
      default:
        return state
    }
  },
}

export default CounterModule

# src/store/index.js

import { combineReducer } from '../utils'
import UserModuel from './user'
import CounterModule from './counter'

const state = {
  user: UserModuel.state,
  counter: CounterModule.state
}

const reducers = combineReducer({
  user: UserModuel.reducer,
  counter: CounterModule.reducer
})

export default {
  state,
  reducers
}

很常规的分模块定义statereducer。 在index.js中,我们使用了combineReducer这个自定义方法将多个reducer合并。这里也是模仿Redux的实现方法写了一个简易的combineReducer

# /src/utils/index.js

export const combineReducer = reducers => (state = {}, action) =>
  Object.keys(reducers).reduce((newState, key) => {
    newState[key] = reducers[key](state[key], action)
    return newState
  }, {})

3 如何使用

在项目根目录的index.js中引入Provider和定义好的store

# src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
import { Provider } from './redux'
import store from './store'

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()

使用ProviderApp包裹,传入store。和使用Redux一模一样的使用方法。

# src/App.js

import React from 'react'
import logo from './logo.svg'
import './App.css'
import { useRedux } from './redux'

function App() {
  const [state, dispatch] = useRedux()

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Count:{state.counter.count}</p>
        <button onClick={() => dispatch({ type: 'increment' })}>+</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        <p>Token:{state.user.token}</p>
        <button onClick={() => dispatch({ type: 'SET_TOKEN' })}>
          SET TOKEN
        </button>
        <button onClick={() => dispatch({ type: 'RESET_TOKEN' })}>
          RESET TOKEN
        </button>
      </header>
    </div>
  )
}

export default App

引入之前定义好的useRedux,开始尽情使用吧。

这只是一个简易的Redux实现,其中代码还有很多瑕疵和需要改进的地方。但是在中小型项目中已经足够使用了。

(动图中的变量名可能和代码有出入,请忽略这个细节,逃)。

本文使用 mdnice 排版