学习React-22-Zustand

Zustand 简介

Zustand 是一个轻量级的状态管理库,适用于 React 应用。它提供简单的 API 和高效的性能,适合中小型项目或需要替代 Redux 的场景。Zustand 的核心特点是基于 Hook 的设计,无需复杂的 Provider 嵌套。

核心特性

  • 极简 API :通过 create 函数创建 store,直接使用 Hook 访问状态。
  • 无需 Provider:状态全局共享,避免组件树嵌套。
  • 高性能:自动优化渲染,仅更新依赖变更的组件。
  • 中间件支持:可扩展日志、持久化等功能。

安装

bash 复制代码
npm install zustand

基本用法

创建 Store

通过 create 方法定义 store,包含状态和更新逻辑:

javascript 复制代码
import { create } from 'zustand';
interface StoreType {
	count: number;
	increment: () => void
}
const useStore = create<StoreType>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));
在组件中使用

直接通过 Hook 访问状态或动作:

javascript 复制代码
function Counter() {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
}

进阶功能

immer

immer中间件通常用于状态管理库(如Redux),允许以不可变的方式直接修改状态。它利用ProxyObject.defineProperty实现"草稿状态"的修改,最终返回新的不可变状态,简化了深层嵌套数据的更新逻辑。

核心功能
  • 不可变性:自动处理状态的不可变更新,避免手动展开(spread)或深拷贝
  • 语法简化:支持直接修改"草稿"对象,如draft.value = 1,内部转换为不可变更新。
  • 性能优化:仅修改变化的部分,未修改的数据保持引用不变。
安装
bash 复制代码
npm install immer
React 用法
复制代码
需要导出produce,然后它的第一个参数是原始值,第二个参数是一个回调函数,回调函数中的参数是draft,也就是原始值的拷贝,然后我们就可以直接修改draft了,最后返回新的值
ts 复制代码
// 1. 纯 Immer,与任何状态库无关
import { produce } from 'immer';

const oldState = {
  user: { name: '张三', age: 18 },
  roles: ['editor']
};

// 更新函数
const nextState = produce(oldState, draft => {
  draft.user.age = 20;        // 想怎么改就怎么改
  draft.roles.push('admin');  // 不需要手写展开符
});

console.log(oldState === nextState); // false( immutable )
console.log(nextState.user.age);     // 20
Zustand用法
ts 复制代码
// store/useTodoStore.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer'; // 记住这里的导入是从zustand里导入的

type Todo = { id: number; title: string; done: boolean };

type Store = {
  todos: Todo[];
  addTodo: (title: string) => void;
  toggleTodo: (id: number) => void;
};

export const useTodoStore = create(
  immer<Store>((set, get) => ({
    todos: [],
    addTodo: title =>
      set(state => {
        state.todos.push({ id: Date.now(), title, done: false }); // 直接 push!
      }),
    toggleTodo: id =>
      set(state => {
        const todo = state.todos.find(t => t.id === id);
        if (todo) todo.done = !todo.done;   // 直接改属性!
      }),
  }))
);
用法的对比
场景 写法 是否必须 Immer
React 本地 useState setState(produce(draft => { ... })) 可选,但省代码
全局轻量 store create(() => ({ ... })) 不需要
全局 深层 store create(immer(() => ({ ... }))) 强烈推荐
Immer 的核心原理一句话概括:

"用 Proxy 把你对草稿的每一次'非法'修改都拦截下来,然后只复制被触碰的节点,最终拼出一棵全新的不可变树,其余部分保持引用不变。"

useShallow
复制代码
useShallow是 Zustand 官方提供的 React Hook 包装器,专门解决"一次选多个状态"时因"每次返回新对象"而导致的 多余重渲染 问题。
它内部做了一件很简单的事:把 selector 的结果做一次浅比较(shallow equal),如果顶层字段都一样,就强制跳过本次渲染。
用法
ts 复制代码
import { useShallow } from 'zustand/react/shallow'   // 推荐,React 版
// or
import { useShallow } from 'zustand/shallow'        // 纯函数版,也能用
小栗子
ts 复制代码
// store.ts
// 1. 状态
interface State {
  count: number;
  name: string;
}

// 2. 方法(Actions)
interface Actions {
  inc: () => void;
  setName: (n: string) => void;
}

// 3. 合并后给组件用的类型(可选)
export type StoreType = State & Actions;

export const useCounter = create<StoreType>(() => ({
  count: 0,
  name: 'Counter',
  inc: () => useCounter.setState(s => ({ count: s.count + 1 })),
  setName: (n: string) => useCounter.setState({ name: n })
}))

// Component.tsx
import { useShallow } from 'zustand/react/shallow'

function Counter() {
  // 只会在 count 或 inc 变化时才渲染
  const { count, inc } = useCounter(
    useShallow(state => ({ 
    	count: state .count, 
    	inc: state .inc 
   	}))
  )

  return (
    <>
      <p>{count}</p>
      <button onClick={inc}>+1</button>
    </>
  )
}

注意:

  1. 只浅比较一层
    如果 selector 返回嵌套对象 { user: state.user },user.id 变化 不会被检测到;此时应把需要的数据平铺到顶层。
  2. 不要在外层包 useMemo
    useShallow 内部已经做了 memo,再包 useMemo 反而增加复杂度。
  3. 和 persist/immer 不冲突
    可以任意组合:create(persist(immer(...))) 后再 useShallow 选状态即可。

中间件

复制代码
zustand 的中间件是用于在状态管理过程中添加额外逻辑的工具。它们可以用于日志记录、性能监控、数据持久化、异步操作等。在 Zustand 里,"中间件"不是 Express 那种"洋葱圈"模型,而是 "高阶函数":

config => newConfig,把原始的 set / get / store 包一层,先拦截、再决定是否往下传。

名称 一句话作用 安装
immer 让你直接改 state 却生成不可变值 zustand/middleware/immer
persist 把状态同步到 localStorage / IndexedDB zustand/middleware/persist
devtools 接入 Redux DevTools 时间旅行 zustand/middleware/devtools
subscribeWithSelector store.subscribe 加 selector + 相等比较 zustand/middleware/subscribeWithSelector
combine 快速把"状态"和"动作"拼在一起(少写一次类型) zustand/middleware/combine
immer上文已经介绍
persist(数据固化)
ts 复制代码
import { persist, createJSONStorage } from 'zustand/middleware'

export const useStore = create<Store>()(
  persist(
    (set, get) => ({
      count: 0,
      name: '张三',
      inc: () => set({ count: get().count + 1 })
    }),
    {
      name: 'counter-storage',                // localStorage key(唯一)
      storage: createJSONStorage(() => localStorage), // sessionStorage、localStorage
      // 可以选择需要存储的数据
      partialize: (s) => ({ count: s.count }) // 只持久化 count
    }
  )
)

注:persist另外提供了一个清楚存储的方法clearStorage

ts 复制代码
export default function UseStore() {
  console.log("Store组件渲染~");
  // const { price, incrementPrice, decrementPrice, resetPrice } = usePriceStore();
  const {price, incrementPrice, decrementPrice, resetPrice } = usePriceStore(useShallow((state) => ({
    price: state.price,
    incrementPrice: state.incrementPrice,
    decrementPrice: state.decrementPrice,
    resetPrice: state.resetPrice
  })))
  const { gourd, updateGourd } = useUserStore();

  function getUser() {
    updateGourd();
    console.log(gourd);
    
  }
  return (
    <div>
      <Flex vertical className="card">
        <h1>测试Zutand</h1>
        <div>Pirce {price}</div>
        <Flex vertical gap={"small"}>
          <Flex justify="space-between" gap={"small"}>
            <Button type="primary" style={{ flex: 1 }} onClick={incrementPrice}>加加</Button>
            <Button type="primary" style={{ flex: 1 }} onClick={decrementPrice}>减减</Button>

          </Flex>
          <Button type="primary" danger onClick={resetPrice}>重置</Button>
          <Button type="primary" danger onClick={usePriceStore.persist.clearStorage}>重置缓存</Button>
          <Button type="primary" danger onClick={getUser}>升级</Button>
        </Flex>
      </Flex>
    </div>
  )
}
devtools(调试)

搭配Redux DevTools调试更简单方便。

ts 复制代码
import { devtools } from 'zustand/middleware'

export const useStore = create<Store>()(
  devtools(
    (set, get) => ({
      count: 0,
      inc: () => set({ count: get().count + 1 }, false, 'inc') // 第三个参数=action type
    }),
    { 
    	name: 'price-store', // store名称
    	enabled: import.meta.env.DEV // 是否开启检测
    }
  )
)
多中间件组合顺序
ts 复制代码
create<Store>()(
  devtools(
    immer(
      persist((set, get) => ({ ... }), { name: 'key' })
    )
  )
)
自定义中间件
ts 复制代码
const logMiddleware: Middleware<Store> = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('prev', get())
      set(...args)
      console.log('next', get())
    },
    get,
    api
  )

export const useStore = create<Store>()(
  logMiddleware((set, get) => ({
    count: 0,
    inc: () => set({ count: get().count + 1 })
  }))
)

订阅

复制代码
zustand 的 subscribe,可以订阅一个状态,当状态变化时,会触发回调函数,可以类比watch监听函数。
最简API
ts 复制代码
const unsub = store.subscribe(
  (state) => state.count,          // selector,可选
  (count, prevCount) => {
   // 变化时触发 count(当前值), prevCount(之前值) 
   count++
   },
  { fireImmediately: true }        // 选项,可选
)

// 取消监听
unsub()
  • 如果不传 selector,默认监听整个 state(引用变化即触发)。
ts 复制代码
const unsub = store.subscribe(
  state => {
  	/* 变化时触发 */ 
   	state.count++
   },
  { fireImmediately: true }        // 订阅瞬间先跑一次(可选)
)
  • 返回的函数调用后即可取消订阅。
两种用法
场景 拿到 store 的方式
在组件外(如 utils、事件总线) import { useStore } from './store'; const store = useStore.getState()
在组件内(useEffect 里) const store = useStore 本身就是 store 对象
ts 复制代码
const store = create((set) => ({
  count: 0,
}));
//外部订阅
store.subscribe((state) => {
  console.log(state.count);
});
//组件内部订阅
useEffect(() => {
  store.subscribe((state) => {
    console.log(state.count);
  });
}, []);
小栗子(组件外打印日志)
ts 复制代码
// store.ts
export const useCounter = create(() => ({
  count: 0,
  inc: () => useCounter.setState(s => ({ count: s.count + 1 }))
}))

// logger.ts(纯 JS 文件)
const unsub = useCounter.subscribe(
  state => state.count,
  (count, prevCount) => {
    console.log('[logger] count', prevCount, '→', count)
  }
)

// 之后任何代码调用 useCounter.getState().inc() 都会触发上面日志
与 React useEffect 结合
ts 复制代码
function CountLabel() {
  useEffect(() => {
    const unsub = useCounter.subscribe(
      state => state.count,
      cnt => {
        document.title = `Counter: ${cnt}` // 副作用:改标题
      }
    )
    return unsub // 组件卸载时取消
  }, [])

  return <span>{useCounter(s => s.count)}</span>
}
订阅整个 state(无 selector)
ts 复制代码
useCounter.subscribe(
  state => state, // 不写也行,默认就是 identity
  (state, prevState) => {
    if (state.count !== prevState.count) {
      // 自己手动 diff
    }
  }
)
subscribeWithSelector 中间件(可选)

如果你希望 selector 返回值做浅比较(而不是引用比较),装这个中间件:

ts 复制代码
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
  subscribeWithSelector(() => ({ count: 0, name: '' }))
)

// 现在 subscribe 的 selector 默认浅比较
useStore.subscribe(
  state => ({ cnt: state.count }), // 返回对象也不会疯狂触发
  ({ cnt }) => console.log(cnt)
)
常见误区
误区 正确理解
subscribe 会触发 React 重新渲染 ❌ 它只跑回调 ,与渲染无关;渲染用 useStore(selector)
必须在组件里才能用 任何地方 都能 store.subscribe
返回的新 state 是深拷贝 ❌ 只是不可变新引用,内部结构共享
相关推荐
东华帝君1 小时前
vue3自定义v-model
前端
fruge1 小时前
搭建个人博客 / 简历网站:从设计到部署的全流程(含响应式适配)
前端
光影少年1 小时前
css影响性能及优化方案都有哪些
前端·css
呆呆敲代码的小Y2 小时前
2025年多家海外代理IP实战案例横向测评,挑选适合自己的
前端·产品
q***3752 小时前
爬虫学习 01 Web Scraper的使用
前端·爬虫·学习
v***5652 小时前
Spring Cloud Gateway
android·前端·后端
b***59432 小时前
分布式WEB应用中会话管理的变迁之路
前端·分布式
q***21602 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
顾安r2 小时前
11.21 脚本 网页优化
linux·前端·javascript·算法·html