Published on

dva 阵痛迁移至 zustand

前言

在进行移动端项目体积优化时发现项目中使用到了 dva,而 dva 又使用到了 immer, 这两个库的体积加起来达到了 117kb+16.2kb,同时 dva 本身的学习成本、ts 支持度也不够友好,所以通过当前社区火热的一个状态管理库 zustand 来替换 dva,而达到缩小体积、提高开发体验的目的。

zustand vs dva

dva 使用 Generator 函数,zustand 使用正常的方法,支持异步。

zustand 体积更小,更简洁的 API,更友好的 ts 支持。在命名空间、扩展性等功能上都有良好的支撑。

size

namesize
zustand3kb
dva117kb

api

下方 api 都会从 dva 的角度进行展开

创建仓库

dva

// 创建应用
const app = dva()

// 注册 Model
app.model({
  namespace: 'count',
  state: 0,
  reducers: {
    add(state) {
      return state + 1
    },
  },
  effects: {
    *addAfter1Second(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'add' })
    },
  },
})

zustand

import create from 'zustand'

const useStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

声明 state

dva

const app = dva()

interface ModelState {
  foo: number
}

interface Model {
  state: ModelState
}

const INITIAL_STATE: ModelState = {
  foo: 0,
}

const model: Model = {
  namespace: 'count',
  state: INITIAL_STATE,
  reducers: {},
  effects: {},
}

// 注册 Model
app.model(model)

zustand

import create from 'zustand'

interface State {
  foo: number
}

type Store = State

const INITIAL_STATE: State = {
  foo: 0,
}

const useStore = create<Store>((set) => ({
  ...INITIAL_STATE,
}))

声明 reducers

dva

const app = dva()

interface ModelState {
  foo: number
}

interface Reducers {
  setFoo(state: ModelState, payload: { foo: number }): void
}

interface Model {
  state: ModelState
  reducers: Reducers
}

const INITIAL_STATE: ModelState = {
  foo: 0,
}

const model: Model = {
  namespace: 'count',
  state: INITIAL_STATE,
  reducers: {
    setFoo(state, payload) {
      return {
        ...state,
        foo: payload.foo,
      }
    },
  },
  effects: {},
}

// 注册 Model
app.model(Model)

zustand

import create from 'zustand'

interface State {
  foo: number
}

interface Reducers {
  setFoo(foo: number): void
}

const INITIAL_STATE: State = {
  foo: 0,
}

type Store = State & Reducers

const useStore = create<Store>((set) => ({
  ...INITIAL_STATE,
  setFoo(foo: number) {
    set({
      foo,
    })
  },
}))

声明 effects

dva

const app = dva()

interface ModelState {
  foo: number
}

interface Reducers {
  setFoo(state: ModelState, payload: { foo: number }): void
}

interface Effects {
  asyncSetFoo(action: any, params: { call: any; put: any }): void
}

interface Model {
  state: ModelState
  reducers: Reducers
  effects: Effects
}

const INITIAL_STATE: ModelState = {
  foo: 0,
}

const model: Model = {
  namespace: 'model',
  state: INITIAL_STATE,
  reducers: {
    setFoo(state, payload) {
      return {
        ...state,
        foo: payload.foo,
      }
    },
  },
  effects: {
    *asyncSetFoo(action, { call, put }) {
      yield call(delay, 1000)
      yield put({ type: 'setFoo', payload: { foo: 1 } })
    },
  },
}

// 注册 Model
app.model(Model)

zustand

import create from 'zustand'

interface State {
  foo: number
}

interface Reducers {
  setFoo(foo: number): void
}

interface Effects {
  asyncSetFoo(): void
}

const INITIAL_STATE: State = {
  foo: 0,
}

type Store = State & Reducers & Effects

const useStore = create<Store>((set) => ({
  ...INITIAL_STATE,
  setFoo(foo: number) {
    set({
      foo,
    })
  },
  async asyncSetFoo() {
    await delay(1000)
    set({
      foo: 1,
    })
  },
}))

TS 支持度对比

dva

import React from 'react'
import { useSelector, useDispatch } from 'dva'

interface RootState {
  foo: ModelState
}

const Component: React.FC = () => {
  const model = useSelector<RootState, ModelState>((state) => state.foo) // selector 需要手动声明/关联
  const dispatch = useDispatch()

  const handleClick = () => {
    dispatch({
      type: 'model/effectName',
      payload: {
        // payload 需要手动声明/关联
        foo: 1,
      },
    })
  }

  return <div onClick={handleClick}>{model.foo}</div>
}

export default Component

zustand

import React from 'react'
import useStore from './store'

const Component = () => {
  const store = useStore() // 自动关联

  return (
    <div
      onClick={() => {
        store.asyncSetFoo() // 自动关联
      }}
    >
      {store.foo}
    </div>
  )
}

export default Component
codesandbox

zustand 替换 dva 实操

实现 model 内的 call

// dva
export default {
  namespace: 'model',
  state: {
    field: [],
  },
  reducers: {},
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      const result = yield call(delay, 1000)
    },
  },
}

// zustand
const useStore = create((set, get) => ({
  async asyncSetFoo() {
    await delay(1000)
  },
}))

实现 model 内的 put

同 model put

// dva
export default {
  namespace: 'model',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      yield put('foo', { payload: {} })
    },
  },
}

// zustand
const useStore = create((set, get) => ({
  foo() {},
  async asyncSetFoo() {
    get().foo()
  },
}))

不同 model put

// dva
// model1
export default {
  namespace: 'model1',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      yield put('foo', { payload: {} })
    },
  },
}

// model2
export default {
  namespace: 'model2',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      yield put('model1/asyncFoo', { payload: {} })
    },
  },
}

// zustand
// store1
const useStore1 = create((set, get) => ({
  foo() {},
  async asyncSetFoo() {
    get().foo()
  },
}))

// store2
const useStore2 = create((set, get) => ({
  foo() {},
  async asyncSetFoo() {
    await useStore1.getState().asyncSetFoo()
  },
}))

dva zustand 共存 put

// dva
// model1
export default {
  namespace: 'model1',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      // dva call zustand
      yield call(useStore1.getState().asyncSetFoo)
    },
  },
}

// zustand
// store1
const useStore1 = create((set, get) => ({
  foo() {},
  async asyncSetFoo() {
    // zustand use dva
    getDvaApp()._store.dispatch({ type: 'model1/asyncFoo', payload: {} })
  },
}))

实现 model 内的 select

同 model select

// dva
export default {
  namespace: 'model',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      const model = yield select((state) => state.model)
      console.log(model.field)
    },
  },
}

// zustand
const useStore = create((set, get) => ({
  field: [],
  foo() {},
  async asyncSetFoo() {
    console.log(get().field)
  },
}))

不同 model select

// dva
// model1
export default {
  namespace: 'model1',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      yield put('foo', { payload: {} })
    },
  },
}

// model2
export default {
  namespace: 'model2',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      const model = yield select(state => state.model1)
      console.log(model.field)
    },
  },
}

// zustand
// store1
const useStore1 = create((set, get) => ({
  field: [],
  foo() {},
  async asyncSetFoo() {
    get().foo()
  },
}))

// store2
const useStore2 = create((set, get) => ({
  field: [],
  foo() {},
  async asyncSetFoo() {
    console.log(useStore1.getState().field)
  },
}))

dva zustand 共存 select

// dva
// model1
export default {
  namespace: 'model1',
  state: {
    field: [],
  },
  reducers: {
    foo() {},
  },
  effects: {
    *asyncFoo({ payload }, { call, put, select }) {
      // dva call zustand
      console.log(useStore1.getState().field)
    },
  },
}

// zustand
// store1
const useStore1 = create((set, get) => ({
  field: [],
  foo() {},
  async asyncSetFoo() {
    const result = getDvaApp()._store.getState().model
    console.log(result.field)
  },
}))

实现 model 内的 subscriptions

查看了下项目中使用的地方几乎为 0,大多数为监听 history 然后进行对应操作。

如果需要迁移可以考虑通过 subscribe api 及在全局组件上监听 history 实现

代码迁移

跟随上方代码进行对应 api 替换即可。需要注意的是单独的 model 必须整体迁移。

参考链接