在react中使用状态管理工具Zustand以及多种代码组织方式

9,375 阅读6分钟

Zustand使用以及在大型应用下的代码组织方式

介绍

zustand是一个精简、快速、可扩展的状态管理器,特别契合react hook下开发。使用上非常简单,在一些特殊场景下也能优雅适配。是我目前使用过之后最喜欢的一个状态管理器

对比redux优势

使用上比redux要简单明了的多,不需要自己去安装各种中间件,不需要包裹在 context provider,同时redux适用的场景zustand都能满足。

也解决了redux dispatch派发action时编辑器无法点击跳转对应文件的问题(手动查找对应文件真的很烦)

缺点:原生不支持class组件,不过可以通过高阶组件包装一层,或者使用组件外setState getState,使用context模式也行

使用

简单使用
import './App.css';
import { create } from 'zustand'

const useStore = create((set, get) => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}))

function Controls() {
  const inc = useStore(state => state.inc)
  return <button onClick={inc}>one up</button>
}

function Counter() {
  const count = useStore(state => state.count)
  return <h1>{count}</h1>
}
function App() {
  return (
    <div className="App">
      <Counter/>
      <Controls/>
    </div>
  );
}

export default App;

在codesandbox查看

使用上就是这么简单,只需要定义一个对象store,不需要区分state还是action,使用上获取状态与更新状态也只需要一个useStoreAPI即可

详细使用
import './App.css';
import {create} from 'zustand'

const useBearStore = create((set, get) => ({
    bears: 0,
    honey: 0,
    // 获取蜂蜜数量
    getHoneyCount: () => {
        // 在action中使用get读取状态
        const bears = get().bears
        set({honey: bears})
    },
    // 杀死n只蜜蜂
    // 异步action
    killBear: async (number) => {
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                set((state) => ({bears: state.bears - number, honey: state.bears - number}))
            }, 2000)
        })
    },
    // set 函数第二个参数默认为 false,即合并值而非覆盖整个 store,可以利用这个特性清空 store,注意包括action也会清除
    deleteEverything: () => set({}, true),


    // actions 是不变的静态函数,它更新 state 中的值, 但它不是真正的“状态”(state)
    actions: {
        increasePopulation: () => set((state) => ({bears: state.bears + 1})),
        removeAllBears: () => set({bears: 0})
    },

}))
export const useBears = () => useBearStore((state) => state.bears)

// 可以只使用一个 hooks 导出所有的 actions,因为内部的action定义完后不会修改,也就不会触发组件的重新渲染
export const useBearActions = () => useBearStore((state) => state.actions)

export const useKillBear = () => useBearStore((state) => state.killBear)


function Counter() {
    const bears = useBears()
    // const honey = useBearStore((state) => state.honey)
    // 一个错误的用法,这里看起来简洁了很多,也能拿到 honey 正常使用并更新state
    // 但是这里订阅了整个store的状态,任意状态更新都会引起Counter组件的重新渲染
    const {honey} = useBearStore()

    return <>
        <h1>{bears} around here ...</h1>
        <h1>honeys count:{honey} </h1>
    </>
}


function Controls() {
    const {increasePopulation} = useBearActions()
    return <div>
        <button onClick={() => {
            increasePopulation(2);
        }}>one up
        </button>
    </div>
}

function QueryHoney() {
    const getHoneyCount = useBearStore((state) => state.getHoneyCount)
    return <div>
        <button onClick={getHoneyCount}>query honey</button>
    </div>
}

function KillBear() {
    const num = 1
    const killBear = useKillBear()
    return <div>
        <button onClick={() => {
            killBear(num)
        }}>{`kill ${num} bear`}</button>
    </div>
}

function App() {
    return (
        <div className="App">
            <Counter/>
            <Controls/>
            <QueryHoney/>
            <KillBear/>
        </div>
    );
}

export default App;

在codesandbox查看上述事例代码

上述事例包含了 全部状态获取(谨慎使用)、从action中读取“状态(state)”、覆盖状态(set函数有第二个参数,默认为false。它将替换状态而不是合并它们)

上述代码事例为了演示方便写到了一个文件中,实际使用中stateaction比较多,可以将stateaction hooks这两块代码拆分成两个文件引入使用,根据实际需求自由调整或参考下面的组织方式。

initialState、createStore、自定义hooks式组织方式

该方式比较适合复杂的项目或者状态控制非常多的业务组件,可以清晰明了的组织代码

  1. store目录结构

    ./store
    ├─createStore.ts ----- // 负责创建 Store 的方法与 Action 方法
    ├─index.ts ----------- // 导出所有类型、状态与hooks
    ├─initialState.ts ---- // 负责 State —— 添加状态类型与初始化状态值
    └─useState.ts -------- // 自定义state hooks,方便复用与筛选查询
    

2.各个文件具体代码

initialState.ts

/*
* 负责 State —— 添加状态类型与初始化状态值
*
* */

// 登录用户信息
export interface User {
    userInfo: {
        userId: number | undefined;
        nickName: string;
        userName: string;
        portraitUrl?: string;
        favoriteCatCategory: number | undefined
    }
}

// 用户信息初始值
export const initUser: User = {
    userInfo: {
        userId: undefined,
        nickName: '',
        userName: '',
        favoriteCatCategory: undefined,
    }
}

export interface Token {
    accessToken: string | null;
    refreshToken: string | null;
}

interface Dog {
    dog: {
        loading: boolean;
        url?: string
    }
}

interface Cat {
    cat: {
        loading: boolean;
        url?: string
    }
}

// token信息初始值
export const initToken: Token = {
    accessToken: null,
    refreshToken: null,
}

export const initDog: Dog = {
    dog: {
        loading: false,
        url: ''
    }
}

export const initCat: Cat = {
    cat: {
        loading: false,
        url: ''
    }
}

export type State = User & Token & Dog & Cat

export const initialState: State = {
    ...initUser,
    ...initToken,
    ...initDog,
    ...initCat
};

createStore.ts

/*
* 负责创建 Store 的方法与 Action 方法
*
* */
import {create} from 'zustand';
import {persist} from "zustand/middleware";

import type {State} from './initialState';
import {initialState, initUser, initToken} from './initialState';
import {login} from "../services/login";


interface Action {
    resetUser: () => void;
    resetToken: () => void;
    login: (params: { username: string; password: string }) => Promise<boolean>
}

export type Store = State & Action;

// 这里加了持久化中间件persist,数据会储存在localStorage或者sessionStorage(选用session需要配置)里,
// 刷新和关闭网页之后,数据依然能恢复
// 其他比如devtools等中间件类似用法,再次包裹复合使用也可以
export const useStore = create<Store>()(persist(
    (set, get) => ({
        ...initialState,

        // 重置用户信息
        resetUser: () => {
            set({...initUser});
        },
        // 重置token信息
        resetToken: () => {
            set({...initToken});
        },
        login: async (params) => {
            const res = await login(params)
            console.log('login: ', res)

            if (res) {
                localStorage.setItem('accessToken', res.accessToken)
                localStorage.setItem('refreshToken', res.refreshToken)
                set({...res})

                return true
            } else {
                return false
            }
        },
    }), {
        name: 'app-storage'
    }
))

useState.ts

/*
* 自定义state hooks
* 可以根据参数自定义查询state
* 也可以先查询其他state的数据,根据数据值再查询想要的数据
*
* */
import {useStore} from './createStore';
import {shallow} from "zustand/shallow";

// 提现shallow的作用,可以尝试去掉shallow参数,去Shallow页面点击修改userId和userName的按钮,
// 查看控制台打印值,对比二者的打印值,体会shallow的左右(浅比较在性能优化方面的作用)
//
export const useTokenAndUserName = (type?: string) => useStore((state) => {
    if (type && type === 'user') {
        return ({
            userName: state.userInfo.userName,
            accessToken: state.accessToken,
            refreshToken: state.refreshToken
        })
    } else {
        return ({
            accessToken: state.accessToken,
            refreshToken: state.refreshToken
        })
    }

}, shallow)
export const useAccessToken = () => useStore((state) => state.accessToken)
export const useRefreshToken = () => useStore((state) => state.refreshToken)
export const useUserInfo = () => useStore((state) => state.userInfo)

export const useUserInfoExist = () => {
    const accessToken = useAccessToken()
    const refreshToken = useRefreshToken()
    const userInfo = useUserInfo()
    if (accessToken && refreshToken) {
        return {
            exist: true,
            userInfo: userInfo
        }
    } else {
        return {
            exist: false
        }
    }
}

index.ts

export {useStore} from './createStore';
export type {Store} from './createStore';
export type {State} from './initialState';
export * from './useState';

在codesandbox查看上述事例代码

上述事例不止包含了store的组织方式,还包含了使用shallow进行性能优化,组件外对状态进行获取/修改,persist中间件使用

事例代码在分别在utils /request.ts store/useState.ts这两个文件中,有代码注释进行说明

切片组织方式

该方式比较适合功能模块独立且非常多的超大型项目,每个功能模块state与action单独抽取维护

  1. store目录结构
./store
├─cat.ts 
├─createStore.ts 
├─dog.ts 
├─index.ts 
├─token.ts 
├─useState.ts 
└─user.ts 
  1. 关键代码演示

token.ts

import {StateCreator} from 'zustand'
import {login} from "../services/login";
import {Store} from "./createStore";
import {persist} from "zustand/middleware";

export interface Token {
    accessToken: string | null;
    refreshToken: string | null;
    resetToken: () => void;
    login: (params: { username: string; password: string }) => Promise<boolean>
}

export const initToken = {
    accessToken: null,
    refreshToken: null,
}
export const createToken: StateCreator<
    Store,
    [],
    [["zustand/persist", Token]],
    Token
> = persist(
    (set) => ({
        accessToken: null,
        refreshToken: null,
        // 重置token信息
        resetToken: () => {
            set({...initToken});
        },
        login: async (params) => {
            const res = await login(params)
            console.log('login: ', res)

            if (res) {
                localStorage.setItem('accessToken', res.accessToken)
                localStorage.setItem('refreshToken', res.refreshToken)
                set({...res})

                return true
            } else {
                return false
            }
        },
    }), {name: 'zustand-slice'}
)

createStore.ts

import {create} from 'zustand';
import {createUser, User} from "./user";
import {createToken, Token} from "./token";
import {createCat, Cat} from "./cat";
import {createDog, Dog} from "./dog";


export type Store = User & Token & Cat & Dog;

export const useStore = create<Store>()((...a) => ({
    ...createToken(...a),
    ...createUser(...a),
    ...createCat(...a),
    ...createDog(...a)
}))

在sandbox中查看

zustand组织方式与使用方式很多,几乎能适应react开发中的所有状态场景,这里还有一些中间件没有全部介绍,比如 devtoolsimmersubscribeWithSelectorreduxcombine等,还可以自己写中间件实现特殊需求

相关资料

这几个相关资料我觉得对学习zustand比较有用,分享给大家

zhuanlan.zhihu.com/p/475571377

juejin.cn/post/718246…

github.com/pmndrs/zust…