🌟 为什么选择 Zustand?
Zustand
一个小巧、快速且可扩展的轻量级状态管理解决方案,采用简化的 Flux 原则。它提供了一个基于 Hook 的API,没有模板化,也不强制使用特定模式。
与 Redux
相比,Zustand
与 React 集成更为便捷,并且具备以下显著优势:
- 简单的 API:不强制要求使用特定的模式或架构,让开发者能够更自由地编写代码。
- 符合现代 React 开发习惯 :以
React Hook
作为状态管理的主要方式,使代码更简洁、易读。 - 简化代码结构:无需将整个应用包裹在 Context Provider 中,减少了不必要的代码嵌套。
- 精准更新:可以精准通知组件状态的变更,避免不必要的重新渲染,提升应用性能。
🚀 快速入门
安装 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
发生变化后出现弹框
如果需要获取store
的state
,但又不希望触发组件的重新渲染,可以也可以使用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>
)
}