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)
), [])