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属性:
其他
代码中还有封装的 useGetStore 用于快捷获取state中的属性, 配合ts使用,自动提示state的属性,也很方便