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>
    )
}
相关推荐
若云止水5 小时前
ngx_conf_handler - root html
服务器·前端·算法
佚明zj5 小时前
【C++】内存模型分析
开发语言·前端·javascript
知否技术6 小时前
ES6 都用 3 年了,2024 新特性你敢不看?
前端·javascript
最初@7 小时前
el-table + el-pagination 前端实现分页操作
前端·javascript·vue.js·ajax·html
知否技术8 小时前
JavaScript中的闭包真的过时了?其实Vue和React中都有用到!
前端·javascript
Bruce_Liuxiaowei8 小时前
基于Flask的防火墙知识库Web应用技术解析
前端·python·flask
zhu_zhu_xia8 小时前
vue3中ref和reactive的差异分析
前端·javascript·vue.js
拉不动的猪8 小时前
刷刷题45 (白嫖xxx面试题1)
前端·javascript·面试
幼儿园技术家8 小时前
使用SPA单页面跟MPA多页面的优缺点?
前端
还是鼠鼠8 小时前
认识 Express.js:Node.js 最流行的 Web 框架
开发语言·前端·javascript·vscode·node.js·json·express