Zustand入门教程

🌟 为什么选择 Zustand?

Zustand一个小巧、快速且可扩展的轻量级状态管理解决方案,采用简化的 Flux 原则。它提供了一个基于 Hook 的API,没有模板化,也不强制使用特定模式。

Redux 相比,Zustand 与 React 集成更为便捷,并且具备以下显著优势:

  1. 简单的 API:不强制要求使用特定的模式或架构,让开发者能够更自由地编写代码。
  2. 符合现代 React 开发习惯 :以 React Hook 作为状态管理的主要方式,使代码更简洁、易读。
  3. 简化代码结构:无需将整个应用包裹在 Context Provider 中,减少了不必要的代码嵌套。
  4. 精准更新:可以精准通知组件状态的变更,避免不必要的重新渲染,提升应用性能。

🚀 快速入门

安装 zustand

复制代码
npm install zustand

你的第一个 store

以下是一个简单的示例,展示如何创建一个包含计数功能的 store

typescript 复制代码
import { create } from 'zustand'

type CountState = {
  count: number
  increase: () => void
  reset: () => void
}

const useCountStore = create<CountState>((set) => ({
  count: 0,
  increase() {
    set((state) => ({ count: state.count + 1 }))
  },
  reset() {
    set({ count: 0 })
  },
}))

这里的 useCountStore 是一个自定义 Hook,你可以在其中添加更多的状态和方法。

在React组件中使用 store

javascript 复制代码
import { Button, Space } from 'antd'

function Counter() {
  const count = useCountStore((state) => state.count)
  const increase = useCountStore((state) => state.increase)
  const reset = useCountStore((state) => state.reset)
  
  return (
    <>
    <Space direction={'vertical'} >
      <Button onClick={increase}>递增</Button>
      <Button onClick={reset}>重置</Button>
      <div>
        当前计数:{count}
      </div>
    </Space>
    </>
  )
}

🎯 核心特性

useShallow 渲染优化

useShallow 允许你同时选择多个状态,并且能帮助减少 React 组件不必要的渲染。示例如下:

less 复制代码
const {
    count,
    increase,
    reset,
} = useCountStore(useShallow(state => ({
    count: state.count,
    increase: state.increase,
    reset: state.reset,
})))

下面通过一个完整的示例来展示 useShallow 的作用:

javascript 复制代码
import { Button, message } from 'antd'
import { useEffect } from 'react'

import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

type ChatState = {
    chats: Record<string, string>,
    changeHerAnswer: () => void
}

const useCountStore = create<ChatState>((set) => ({
    chats: {
      "我": "一起去看🎞️吧",
      "她": "好的😉",
  },
    changeHerAnswer() {
        set((state) => ({ chats: {...state.chats, "她": "没空🙃" } }))
    }
}))

function Chat() {
  const changeHerAnswer = useCountStore((state) => state.changeHerAnswer)
  const chats = useCountStore(state => state.chats)
  return (<>
    <Button 
	    color="danger" 
	    variant="solid" 
	    onClick={() =>changeHerAnswer()}>
	    女神改变主意了
	  </Button>
    {
      Object.entries(chats).map(([role, answer]) => (
        <div key={role}>
          <span className='font-semibold'>{role}</span>:
          <span className=''>{answer}</span>
        </div>
      ))
    }
  </>)
}

function ChatRoleReRender() {
  const chats = useCountStore(state => state.chats)
  const chatRoles = Object.keys(chats)
  const [messageApi, contextHolder] = message.useMessage();
  useEffect(() => {
    messageApi.warning('ChatRoleReRender组件渲染了');
  })
  return (<>
    {contextHolder}
    <div>参与对话的角色有{ chatRoles.join('和') }</div>
  </>)
}

function ChatRole() {
  const chatRoles = useCountStore(useShallow(state => Object.keys(state.chats)))
  const [messageApi, contextHolder] = message.useMessage();
  useEffect(() => {
    messageApi.info('ChatRole组件渲染了');
  })
  return (<>
    {contextHolder}
    <div>参与对话的角色有{ chatRoles.join('和') }</div>
  </>)
}

function Scene() {
  return (
    <div className='mt-6 space-y-3 mx-8'>
      <Chat />
      <ChatRoleReRender />
      <ChatRole />
    </div>
  )
}

export default Scene

点击按钮后,由于useShallow 返回的内容并没有变更,ChatRole 组件不会触发组件的重新渲染。

和React自带的一些钩子函数一样,useShallow 是通过对象的浅比较实现的。如果你希望更准确的控制组件re-render,可以通过createWithEqualityFn 自定义相等性比较函数。

在action中读取属性

set 函数之外获取 state 的值,示例如下

typescript 复制代码
import { create } from 'zustand'

type CountState = {
  count: number
  increase: () => void
}

const useCountStore = create<CountState>((set, get) => ({
  count: 0,
  increase() {
    const count = get().count
    set(() => ({ count: count + 1 }))
  },
}))

异步操作如此简单

Zustand 支持直接使用 async/await 进行异步操作,无需额外的中间件。

typescript 复制代码
import { Avatar } from 'antd'
import { useEffect } from 'react'

import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

type User = {
    login: string,
    avatar_url: string,
}

type UserState = {
  users: User[],
  listUser: () => void
}

const useUserStore = create<UserState>((set) => ({
  users: [],
  async listUser() {
    const res = await fetch('https://api.github.com/search/users?q=cat&per_page=3')
    const users = (await res.json() as any)?.items || []
    set({ users })
  },
}))

function User() {
    const users = useUserStore(state => state.users)
    const listUser = useUserStore(state => state.listUser)
    useEffect(() => {
        listUser()
    }, [])
  return (
    <div className="space-y-2 m-4 inline-block">
        {
            users.map(user => (
                <div className='border border-solid border-gray-300 rounded-lg 
                flex items-center gap-2 px-6 py-2 '>
                <Avatar src={user.avatar_url} />
                <div>{ user.login }</div>
            </div>
            ))
        }
    </div>
  )
}

export default User

在组件之外使用store

typescript 复制代码
import { create } from "zustand"

type PersonState = {
    name: string,
    age: number,
    changeAge: (age: number) => void,
}

const usePersonStore = create<PersonState>((set) => ({
    name: 'John',
    age: 20,
    changeAge(age: number) {
        set({ age })
    },
}))

console.log(usePersonStore.getState().name)
usePersonStore.setState({ age: usePersonStore.getState().age + 1 })

export default function Outside() {
    const name = usePersonStore(state => state.name)
    const age = usePersonStore(state => state.age)
    return (
        <div className="flex gap-2 m-4">
            <div>{ name }</div>
            <div>{ age }</div>
        </div>
    )
}

输出结果年龄加+1

有一点需要注意,在组件中使用getState 方法获取的state不会触发组件的重新渲染。

更新嵌套的State

我们可以借助immer 库简化深层状态的更新

复制代码
npm install immer
typescript 复制代码
import { Button } from "antd"
import { produce } from "immer"
import { create } from "zustand"

interface Pet {
    name: string,
    age: number,
}

interface User {
    name: string,
    age: number,
    pets: Pet[]
}

interface UserState {
    user: User,
    increaseAge: () => void,
}

const useUserStore = create<UserState>((set) => ({
    user: {
        name: '靓仔',
        age: 20,
        pets: [
            { name: 'Tom', age: 2 },
            { name: 'Jerry', age: 3 },
        ],
    },
    increaseAge() {
        set(produce((state: UserState) => {
            state.user.age++
            state.user.pets.forEach(pet => {
                pet.age++
            })
        }))
    },
}))

export default function ImmerDemo() {
    const user = useUserStore(state => state.user)
    const increaseAge = useUserStore(state => state.increaseAge)
    return (
        <div className="space-y-2 mt-6 text-center">
            <Button type="primary" onClick={() => increaseAge()}>年龄+1</Button>
            <div>{user.name}: {user.age}</div>
            {
                user.pets.map(pet => (
                    <div key={pet.name}>{pet.name}: {pet.age}</div>
                ))
            }
        </div>
    )
}

同时修改主人和宠物的年龄

使用selector

如果 useShallow 中代码比较复杂,可以将其单独提取出来当做selector 使用

javascript 复制代码
// 根据上面一个例子的代码改造,省略部分代码

const petNameListSelector = (state: UserState) => {
    return state.user.pets.map(pet => pet.name)
}

export default function SelectorDemo() {
    const petNames = useUserStore(useShallow(petNameListSelector))
    return (
        <div className="mt-6 text-center">
            宠物有:{petNames.join(', ')}
        </div>
    )
}

🔍 状态监听与订阅

typescript 复制代码
import { Button, message } from "antd";
import { useEffect } from "react";
import { create } from "zustand";
import { subscribeWithSelector } from 'zustand/middleware'

interface CountState {
    count: number,
    increase: () => void,
}

const useCountStore = create<CountState>()(
    subscribeWithSelector(
        (set) => ({
            count: 0,
            increase() {
                set((state) => {
                    const rand = Math.round(Math.random() * 100)
                    return { count: state.count +  rand}
                })
            },
        })
    )
)

export default function SubscribeDemo() {

    const count = useCountStore((state) => state.count)
    const increase = useCountStore((state) => state.increase)
    const [messageApi, contextHolder] = message.useMessage();
    useEffect(() => useCountStore.subscribe(
        state => state.count, 
        (count, prevCount) => {
            messageApi.info(`Count增大了${count - prevCount}`)
        }
    ) , [])

    return (<>
        {contextHolder}
        <div className="mt-24 text-center space-y-2">
            <h1>Count: {count}</h1>
            <Button type="primary" onClick={increase}>Increase</Button>
        </div>
    </>)
}

我们监听了count属性,count 发生变化后出现弹框

如果需要获取storestate ,但又不希望触发组件的重新渲染,可以也可以使用subscribe

javascript 复制代码
export default function SubscribeDemo() {

    const increase = useCountStore((state) => state.increase)
    const count = useRef<number>(useCountStore.getState().count)
    const [messageApi, contextHolder] = message.useMessage();
    useEffect(() => useCountStore.subscribe(
        state => state.count, 
        value => count.current = value
    ) , [])

    useEffect(() => {
        messageApi.info(`SubscribeDemo渲染`)
    })

    function showCount() {
        messageApi.info(`count=${count.current}`)
    }

    return (<>
        {contextHolder}
        <div className="mt-24 text-center space-y-2">
            <h1>Count: {count.current}</h1>
            <Button type="primary" onClick={increase}>Increase</Button>
            <Button 
	            className="ml-2" 
	            type="primary" 
	            onClick={showCount}>
	            Show Count
	          </Button>
        </div>
    </>)
}

点击 Increase 后视图没有重新渲染,但其实Count已经发生变化

🧩 切片模式

随着功能的增加,store 可能会变得越来越臃肿,越来越难以维护。你可以使用切片模式将主 store 拆分为多个独立的小 store 来实现模块化。

typescript 复制代码
import { Button } from "antd";
import { StateCreator, create } from "zustand";

type BearState = {
    bears: number;
    addBear: () => void;
}
const useBearSlice: StateCreator<BearState, [], [], BearState> = (set) => ({
    bears: 0,
    addBear: () => set((state) => ({ bears: state.bears + 1 }))
})

type FishState = {
    fishes: number;
    addBee: () => void;
}
const useBeeSlice: StateCreator<FishState, [], [], FishState> = (set) => ({
    fishes: 0,
    addBee: () => set((state) => ({ fishes: state.fishes + 1 }))
})

type BoundSlice = {
    addBearAndFish: () => void;
}
const useBoundStore: StateCreator<
    BearState & FishState, 
    [], 
    [], 
    BoundSlice
> = (set, get) => ({
    addBearAndFish: () => {
        get().addBear();
        get().addBee();
    },
})

const useStore = create<BearState & FishState & BoundSlice>()(
    (...args) => ({
       ...useBearSlice(...args),
       ...useBeeSlice(...args),
       ...useBoundStore(...args),
    })
)

export default function SliceDemo() {
    const store = useStore()
    return (
        <div className="my-4 text-center space-y-2">
            <div>森林里,有 {store.bears} 只小熊,有 {store.fishes} 条大鱼</div>
            <div className="space-x-1.5">
                <Button onClick={store.addBear}>Add a Bear</Button>
                <Button onClick={store.addBee}>Add a Bee</Button>
                <Button 
                    type="primary" 
                    onClick={store.addBearAndFish}>
                    Add a Bear and a Bee
                </Button>
            </div>
        </div>
    )
}
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax