Zustand状态管理库

Zustand是一个轻量级、简洁且强大的React状态管理库(Vue用户请使用zustand-vue),旨在为您的React项目提供更简单、更灵活的状态管理方式。与其他状态管理库相比(如Redux/Mobx)Zustand的API更加简洁明了,学习成本更低,且无需引入繁琐的中间件和配置,同时Zustand支持TypeScript,让您的项目更具健壮性。

近几年,React社区出现了很多新的状态管理库,比如Zustand、jotai、recoil等,使用都很简单

Zustand是近年来Star增长最快的React状态管理,函数式的设计理念,全面拥抱hooks,Api设计得很优雅,对业务的入侵小,学习的心智负担低

Zustand中文网:awesomedevin.github.io/zustand-vue...

教程:codthing.github.io/react/zusta...

1、安装Zustand

arduino 复制代码
npm install zustand # or yarn add zustand

2、快速上手DEMO

javascript 复制代码
// 计数器 Demo 快速上手
import React from "react";
import { create } from "zustand";

// create():存在三个参数,第一个参数为函数,第二个参数为布尔值
// 第一个参数:(set、get、api)=>{...}
// 第二个参数:true/false 
// 若第二个参数不传或者传false时,则调用修改状态的方法后得到的新状态将会和create方法原来的返回值进行融合;
// 若第二个参数传true时,则调用修改状态的方法后得到的新状态将会直接覆盖create方法原来的返回值。

const useStore = create(set => ({
  count: 0,
  setCount: (num: number) => set({ count: num }),
  inc: () => set((state) => ({ count: state.count + 1 })),
}));

export default function Demo() {
  // 在这里引入所需状态
  const { count, setCount, inc } = useStore();

  return (
    <div>
      {count}
      <input
        onChange={(event) => {
          setCount(Number(event.target.value));
        }}
      ></input>
      <button onClick={inc}>增加</button>
    </div>
  );
}

3、Selector

获取整个store,它会导致组件在每次状态更改时渲染

ini 复制代码
const state = useStore()

单个state更新渲染,默认情况下,它以严格相等(旧 === 新)检测更改,zustand内部会判断两次返回的值是否一样,如果一样就不会重新渲染。(推荐这种写法)

ini 复制代码
const nuts = useStore(state => state.nuts)
const honey = useStore(state => state.honey)

多个state更新渲染

javascript 复制代码
import shallow from 'zustand/shallow'

// 对象选择,当 `state.nuts` 或 `state.honey` 改变时重新渲染组件
const { nuts, honey } = useStore(state => ({ nuts: state.nuts, honey: state.honey }), shallow)

// 数组选择,当 `state.nuts` 或 `state.honey` 改变时重新渲染组件
const [nuts, honey] = useStore(state => [state.nuts, state.honey], shallow)

// 映射选择,当 `state.treats` 按 `count` 或 `keys` 顺序改变时重新渲染组件
const treats = useStore(state => Object.keys(state.treats), shallow)

‌Zustand的shallow功能主要用于优化组件的渲染性能。‌

在React中,当组件的状态发生变化时,所有使用该状态的组件都会重新渲染。这可能会导致不必要的渲染,尤其是在状态变化不直接影响组件显示内容的情况下。为了解决这个问题,Zustand提供了shallow功能,它可以帮助避免不必要的渲染。

从一个更复杂的例子来学习更多的zustand知识

store的定义

typescript 复制代码
import { create } from 'zustand'
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

export type IInfo = {
  name: string
  age: number
}

export type IBase = {
  info: IInfo
  count: number
  list: string[]
  add: (item: string) => void
  remove: (item: string) => void
  increaseCount: () => void
  increaseInfoAge: () => void
  setName: (name: string) => void
  setAge: (age: number) => void
  setInfo: (info: IInfo) => void
  setNameByImmer: (name: string) => void
  setAgeByImmer: () => void
}

export const useStore = create<IBase>()(
  devtools(
    subscribeWithSelector(
      persist(
        immer((set, get) => ({
          info: {
            name: 'zustand',
            age: 18,
          },
          count: 0,
          list: [],
          setName(name) {
            console.log(get().info)
            set((state) => ({ info: { ...state.info, name } }))
          },
          setAge(age) {
            set((state) => ({ info: { ...state.info, age } }))
          },
          increaseInfoAge() {
            set((state) => ({
              info: { ...state.info, age: state.info.age + 1 },
            }))
          },
          setInfo(info) {
            set(() => ({ info }))
          },
          setNameByImmer(name) {
            set((state) => {
              state.info.name = name
            })
          },
          setAgeByImmer() {
            set((state) => {
              state.info.age += 1
            })
          },
          increaseCount: () => set((state) => ({ count: state.count + 1 })),
          add: (item: string) =>
            set((state) => ({ list: [...state.list, item] })),
          remove: (item: string) =>
            set((state) => ({ list: state.list.filter((i) => i !== item) })),
        })),
        {
          name: 'zustand-store',
          partialize: (state) => ({ list: state.list }),
        },
      ),
    ),
  ),
)

使用store

javascript 复制代码
import { Button } from 'antd'
import { useEffect } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import './App.css'
import { useStore } from './stores'
import { randomString } from './utils'

function About() {
  const count = useStore((state) => state.count)
  const list = useStore((state) => state.list)
  const { name, age } = useStore((state) => state.info)
  const add = useStore((state) => state.add)
  const remove = useStore((state) => state.remove)
  const increaseCount = useStore((state) => state.increaseCount)
  const increaseInfoAge = useStore((state) => state.increaseInfoAge)
  const setName = useStore((state) => state.setName)
  console.log(list)
  
  useEffect(() => {
    const unsub = useStore.subscribe(
      (state) => state.list,
      (newList, preList) => {
        console.log('newList', newList)
        console.log('preList', preList)
      },
    )

    return unsub
  }, [])

  useEffect(() => {
    const unsub1 = useStore.subscribe((state) => {
      console.log('所有变化', state)
    }); // 监听所有变化
    return unsub1; // 取消订阅监听器
  }, [])

  const { name: name2 } = useStore.getState().info
  console.log('name2', name2)

  const addItem = () => {
    add(randomString(12))
  }

  const removeItem = (item: string) => () => {
    remove(item)
  }

  const changeName = () => {
    useStore.setState((state) => ({ info: { ...state.info, name: Math.random().toString()} }))
  }

  return (
    <>
      <div>这是About</div>
      <nav>
        <NavLink to='/about/list'>About List</NavLink>
        <NavLink to='/about/detail'>About Detail</NavLink>
      </nav>
      <Button onClick={increaseCount} type='primary'>
        {count}++
      </Button>
      <Button
        onClick={() => {
          setName(Math.random().toString())
        }}
        type='primary'
      >
        {name2}改变
      </Button>
      <Button onClick={changeName} type='primary'>
        直接改变姓名
      </Button>
      <Button onClick={increaseInfoAge} type='primary'>
        {age}++
      </Button>
      <Button onClick={addItem} type='primary'>
        添加
      </Button>
      <div>
        <span>姓名{name}</span>
        <span>姓名2{name2}</span>
        <span>年龄{age}</span>
      </div>
      <div>
        {list.map((item) => (
          <div key={item}>
            {item} <span onClick={removeItem(item)}>删除</span>
          </div>
        ))}
      </div>
      <Outlet />
    </>
  )
}

export default About

获取非响应式的最新状态

arduino 复制代码
const { name: name2 } = useStore.getState().info
console.log('name2', name2)

store内部也能获取到state的值

scss 复制代码
setName(name) {
  console.log(get().info)
  set((state) => ({ info: { ...state.info, name } }))
},

订阅state的变化

javascript 复制代码
useEffect(() => {
    const unsub1 = useStore.subscribe((state) => {
      console.log('所有变化', state)
    }); // 监听所有变化
    return unsub1; // 取消订阅监听器
  }, [])

使用带选择器的订阅

javascript 复制代码
需要结合subscribeWithSelector使用

useEffect(() => {
    const unsub = useStore.subscribe(
      (state) => state.list,
      (newList, preList) => {
        console.log('newList', newList)
        console.log('preList', preList)
      },
    )

    return unsub
}, [])

中间件

数据持久化 persist

javascript 复制代码
// store.js
import create from 'zustand';
import { persist } from 'zustand-persist';

const initialState = {
  count: 0,
  list: [],
  increment: () => {},
  decrement: () => {},
};

const useStore = create(
  persist(
    (set) => ({
      ...initialState,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }),
    {
      name: 'my-store', // 唯一名称
      getStorage: () => localStorage, // 可选,默认使用 localStorage
      partialize: (state) => ({ list: state.list }), // 只持久化list字段
    }
  )
);

export default useStore;

调试工具 Devtools middle

dart 复制代码
import { devtools, persist } from 'zustand/middleware'

const useFishStore = create(
  devtools(persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
  ))
)

更新不可变状态 Immer

typescript 复制代码
// 引入zustand库和Immer中间件
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'


type UserInfo = {
  username: string
  avatar: string
}

type Action = {
  updateToken: (token: string) => void
  updateUserInfo?: (userInfo: UserInfo) => void
  updateUserName: (username: string) => void
}

interface State {
  token: string
  userInfo: UserInfo
}

// 创建带有Immer中间件的zustand存储
export const useUserStore = create<State & Action>()(

  // 这里使用了immer进行包裹住"设置函数"(setter)
  immer(set => ({
    token: '',
    userInfo: { username: '', avatar: 'http://xxxx.com/yy.jpg' },
    updateToken: token => set(state => { state.token = token }),
    updateUserName: username => set(state => { state.userInfo.username = username })
  }))
)


不使用immer

updateUserName: username =>
set(state => ({
  ...state, // 再次使用展开运算符复制所有既有状态
  userInfo: {
    ...state.userInfo, // 复制 userInfo 对象内的其他属性
    username: username // 只更新 username 属性
  }
}))

瞬时更新ref(用于频繁发生的状态变化)

scss 复制代码
const useStore = create(set => ({ girlNum: 0, ... }))

function Component() {
// 获取初始状态
const girlNumRef = useRef(useStore.getState().girlNum)
// 在挂载时连接到Store,在卸载时断开连接,在引用时捕获状态变化
useEffect(() => useStore.subscribe(
state => (girlNumRef.current = state.girlNum)
), [])
相关推荐
范文杰几秒前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 分钟前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪17 分钟前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy1 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom2 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom2 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom2 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom2 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom2 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI3 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端