小程序使用Redux Toolkit进行状态管理

892 阅读3分钟

背景

之前一直用dva进行状态管理,dva异步处理是基于redux-saga,Generators 语法写起来不方便,ts支持性不好。Redux Toolkit 现在是Redux官方推荐的工具包,集成了redux-thunk、immer等插件,拿来在项目中试用下,

  "dependencies": {
    "immer": "^9.0.7",
    "redux": "^4.1.2",
    "redux-thunk": "^2.4.1",
    "reselect": "^4.1.5"
  },

image.png

安装

yarn add @reduxjs/toolkit 

image.png

基本使用

  1. 注册store
// src/store/index.ts

import { combineReducers, configureStore, createListenerMiddleware } from '@reduxjs/toolkit'
import pageSlice from './features/pageSlice'

export const store = configureStore({
  reducer: {
      user: userSlice,
  },
  // 启用Redux DevTools,默认true
  devTools: false,
  //  默认中间件  [thunk, immutableStateInvariant, serializableStateInvariant] 生产只包含thunk
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    })
})

// src/app.tsx
import { Provider } from 'react-redux'
import { store } from '@/store/index'

class App extends Component {
  // ... ... 省略生命周期
  // 在 App 类中的 render() 函数没有实际作用
  // 请勿修改此函数
  render() {
    return <Provider store={store}>{this.props.children}</Provider>
  }
}
  1. 注册一个用户状态切片
// slice/userSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface UserState {
  count: number
  entities: Array<any>
}

const initialState: UserState = {
  count: 0,
  entities: [],
}

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    // 导出为 actions
    increment(state) {
      state.count += 1
    },
    incrementByStep(state, action: PayloadAction<number>) {
      state.count += action.payload
    },
  },
})

export const { increment, incrementByStep } = userSlice.actions

export default userSlice.reducer // 导出 reducer

这里的reducer和之前使用基本一致,就是更新状态值可以直接通过 state.count = 1 这种方式赋值,当然也可以用之前解构的方式

return { ...state, ...action.payload}

  1. 在页面引入

在这之前为了更好的类型支持,参考官方文档重新定义disptach、selector

// store/index.ts
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// store/hook.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
import React from 'react'
import { View } from '@tarojs/components'
import { fetchUserById, increment, incrementByStep } from '@/store/features/useSlice'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import './index.scss'


const Index = () => {
  const count = useAppSelector((state) => state.user.count)
  const entities = useAppSelector((state) => state.user.entities)
  const source = useAppSelector((state) => state.page.source)
  const dispatch = useAppDispatch()

  // 相当于dispatch({ type: 'counter/increment' })
  const onInc = () => dispatch(increment())
  const onInc2 = () => dispatch(incrementByStep(2))

  console.log('渲染了Index', count)
  return (
    <View className='wrapper'>
      <View className='wrapper' onClick={() => onInc()}>
        {count}
      </View>
      <View className='wrapper' onClick={() => onInc2()}>
        {count}
      </View>
    </View>
  )
}

export default Index

异步请求

redux-toolkit提供了creatAsyncThunk方法处理状态的异步处理

这里也处理下state类型问题

// store/index.ts
export type TypedCreateAsyncThunk<ThunkApiConfig> = <Returned, ThunkArg = void>(
  typePrefix: string,
  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
  options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
) => AsyncThunk<Returned, ThunkArg, ThunkApiConfig>

// store/hook.ts
/**
 *  注:如果返回函数的参数第一个是可选参数,类型使用void
 */
export const createAppAsyncThunk: TypedCreateAsyncThunk<{ state: RootState }> = createAsyncThunk

createAsyncThunk方法返回三种异步状态pending、fulfilled、rejected

redux-toolkit提供了extraReducers去处理这些异步回调

//
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: { 
    // ... 
  },
  // 在extraReducer中可以定义reducer来响应Slice外部的action
  // 比如这里的fetchUserById就是外部定义的Thunk Action
  extraReducers: (builder) => {
    // 异步 actions 中触发与其他 slice 中数据的关联改变
    // 包含本身定义的用户逻辑,pending, fulfilled, rejected 4种action
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'loading'
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.entities.push(action.payload)
      })
      .addCase(fetchUserById.rejected, (state, err) => {
        console.log(state, err)
      })
  },
})

当前也可以不使用extraReducers,通常我们期望的是,在异步方法里可以返回Promise,异步方法间可以相互调用。

官方也提供了unwrap 的方式,用来提取动作payload 返回的数据

import { unwrapResult } from '@reduxjs/toolkit'  
// ...
// 第一种使用方式
  dispatch(fetchUserById(userId)).unwrap()
  // 第二种使用方式
  dispatch(fetchUserById(userId))
    .then(unwrapResult)
    .then((originalPromiseResult) => {
      // handle result here
    })

看了源码,unwrap(){ return promise.then<any>(unwrapResult) } unwrap还是调用的工具的unwrapResult方法

image.png

这里我们使用下

// slice/userSlice.ts
// ...

export const checkCount = createAsyncThunk('user/checkCount', async(_, thunkApi) => {
    const count = thunkApi.getState().user.count
    return count > 0
})

export const fetchUserById = createAsyncThunk('user/fetchUserById', async (userId: number, thunkApi) => {
  const hasCount = await dispatch(checkCount()).unwrap()
  console.log('hasCount', hasCount)
  // hasCount false
  const response: any = await fetchById(userId)
  return response.data
})

日志中间件

除了使用ReduxDevTool工具,有时需要日志定位问题。可以自定义中间件或者redux-logger

const logger = (store) => (next) => (action) => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd()
  return result
}
// yarn add redux-logger
// import logger from 'redux-logger'

export const store = configureStore({
  reducer: { user: userSlice, },
  devTools: false,
  middleware: (getDefaultMiddleware) =>  getDefaultMiddleware().concat(logger),

})

本地化处理

以使用 redux-presist 对 store 的数据进行持久化

// store/index.ts
// 微信小程序持久化api
const storage = {
  async getItem(key) {
    const res = await Taro.getStorage({ key })
    return res.data
  },
  setItem(key, data) {
    return Taro.setStorage({ key, data })
  },
  removeItem(key) {
    return Taro.removeStorage({ key })
  },
  clear: Taro.clearStorage,
}

const userPersistConfig = {
  key: 'user', // 别名
  storage,
  whitelist: ['count'], // 白名单指定需要缓存的键
}

const rootReducer = {
  user: persistReducer(userPersistConfig, userSlice),
}

export const store = configureStore({
  reducer: rootReducer,
  devTools: false,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
})
export const persistor = persistStore(store)
// ...
// app.tsx
import { store, persistor } from '@/store/index'
import { PersistGate } from 'redux-persist/lib/integration/react'
class App extends Component {
  // ...
  render() {
    return (
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          {this.props.children}
        </PersistGate>
      </Provider>
    )
  }
}
export default App

查看调试工具查看下

image.png