react-redux封装计算属性useComputed

602 阅读2分钟

React中没有类似vue中的计算属性(或者是vuex中getters)的api, 本文介绍在react-redux中实现计算属性的hooks

一. 原生写法 useSelector

如果不封装的话,直接使用redux就是这么写:

import { useDispatch, useSelector } from 'react-redux'
import { Button, View } from '@tarojs/components'

const Index: React.FC = () => {
  const count = useSelector((state) => state.counter.count)
  const doubleCount = useSelector((state) => state.counter.count * 2)

  return (
    <>
      <View>
        <View>count:{counter.num}</View>
        <Button
          onClick={() => {
            dispatch(counterModel.actions.add())
          }}
        >
          add count
        </Button>
      </View>

      <View>
        <View>doubleCount:{doubleNum}</View>
      </View>
    </>
  )
}
export default Index

效果:

二. 目录结构

这是一个Taro项目,但是不影响,主要看redux相关的代码。

我把INITIAL_STATE reducer actions 放在了一个文件中作为一个模块,称作model(类似于dva),同时把所有redux相关的东西都放在了store文件夹里

三、代码

store/models/counter.ts

import { IStoreState } from '..'

const INITIAL_STATE = {
  count: 0,
}

const computed = {
  doubleCount(state) {
    return state.count * 2
  },
}

function reducer(state: typeof INITIAL_STATE = INITIAL_STATE, action: { type: string; payload?: any }) {
  switch (action.type) {
    case 'SAVE':
      return { ...state, ...(action.payload ?? {}) }
    default:
      return state
  }
}

const actions = {
  save(payload) {
    return { type: 'SAVE', payload }
  },
  add() {
    return (dispatch, getState: () => IStoreState) => {
      dispatch(
        this.save({
          count: getState().counter.count + 1,
        })
      )
    }
  },
  asyncAdd() {
    return (dispatch, getState: () => IStoreState) => {
      setTimeout(() => {
        dispatch(this.add())
      }, 1500)
    }
  },
}

const model_counter = {
  INITIAL_STATE,
  computed,
  reducer,
  actions,
}

export default model_counter

store/index.ts

import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import { useSelector } from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import model_counter from './models/counter'

/**
 *
 * all models
 */
export const models = {
  counter: model_counter,
  // ...other models
}

/**
 *
 * types
 */
export type IStoreState = {
  [key in keyof typeof models]: (typeof models)[key]['INITIAL_STATE']
}

/**
 *
 * reducer
 */
const rootReducer = combineReducers(
  Object.keys(models).reduce((prev, key) => {
    prev[key] = models[key].reducer
    return prev
  }, {})
)

/**
 *
 * store enhancer
 */
const composeEnhancers =
  typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
        // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
      })
    : compose

const middlewares = [thunkMiddleware]

// // redux 打印日志
// if (process.env.NODE_ENV === 'development') {
//   middlewares.push(require('redux-logger').createLogger())
// }

const enhancer = composeEnhancers(
  applyMiddleware(...middlewares)
  // other store enhancers if any
)

export default function configStore() {
  const store = createStore(rootReducer, enhancer)
  return store
}

/**
 *
 * actions
 */
export const actions = Object.keys(models).reduce((prev, key) => {
  prev[key] = models[key].actions
  return prev
}, {}) as {
  [key in keyof typeof models]: (typeof models)[key]['actions']
}

/**
 *
 * 计算属性
 * @example
 * const doubleCount = useComputed('counter', 'doubleCount')
 */
export function useComputed<T extends keyof typeof models, Y extends keyof (typeof models)[T]['computed']>(
  modelName: T,
  computedName: Y
): // @ts-ignore
ReturnType<(typeof models)[T]['computed'][Y]> {
  const res = useSelector<IStoreState, any>((state) => {
    const computed = models[modelName].computed
    // @ts-ignore
    const getter = computed[computedName]
    return getter(state[modelName])
  })
  return res
}

/**
 * 快捷获取store中的数据
 * @example
 * const counter = useGetStore('counter') // 获取整个counter的数据
 * const num = useGetStore('counter', 'num')
 */
export function useGetStore<T extends keyof typeof models>(modelName: T): (typeof models)[T]['INITIAL_STATE']
export function useGetStore<T extends keyof typeof models, Y extends keyof (typeof models)[T]['INITIAL_STATE']>(modelName: T, stateName: Y): (typeof models)[T]['INITIAL_STATE'][Y]
export function useGetStore<T extends keyof typeof models, Y extends keyof (typeof models)[T]['INITIAL_STATE']>(
  modelName: T,
  stateName?: Y
): (typeof models)[T]['INITIAL_STATE'] | (typeof models)[T]['INITIAL_STATE'][Y] {
  const res = useSelector<IStoreState, any>((state) => {
    if (!stateName) {
      return state[modelName]
    } else {
      return state[modelName][stateName]
    }
  })
  return res
}


关键代码:

/**
 *
 * 计算属性
 * @example
 * const doubleCount = useComputed('counter', 'doubleCount')
 */
export function useComputed<T extends keyof typeof models, Y extends keyof (typeof models)[T]['computed']>(
  modelName: T,
  computedName: Y
): // @ts-ignore
ReturnType<(typeof models)[T]['computed'][Y]> {
  const res = useSelector<IStoreState, any>((state) => {
    const computed = models[modelName].computed
    // @ts-ignore
    const getter = computed[computedName]
    return getter(state[modelName])
  })
  return res
}

四、使用

import React from 'react'
import { actions, useComputed, useGetStore } from '@/store'
import { Button, View } from '@tarojs/components'
import { useDispatch } from 'react-redux'

const Index: React.FC = () => {
  // const count = useSelector((state) => state.counter.count)
  // const doubleCount = useSelector((state) => state.counter.count * 2)

  const dispatch = useDispatch()
  const count = useGetStore('counter', 'count')
  const doubleCount = useComputed('counter', 'doubleCount')

  return (
    <>
      <View>
        <View>count:{count}</View>
        <Button
          onClick={() => {
            dispatch(actions.counter.add())
          }}
        >
          add count
        </Button>
      </View>

      <View>
        <View>doubleCount:{doubleCount}</View>
      </View>
    </>
  )
}
export default Index

因为使用了ts,代码会自动提示拥有的computed属性:

image.png

image.png

其他

代码中还有封装的 useGetStore 用于快捷获取state中的属性, 配合ts使用,自动提示state的属性,也很方便