React 遇上 Zustand:解锁状态管理新姿势 🚀

165 阅读9分钟

作为前端新手,你是不是也遇到过这些崩溃瞬间?—— 组件 A 的状态想传给组件 B,得先传给父组件再转发;页面刷新后,好不容易填的表单数据全没了;用 Redux 写了一堆模板代码,最后发现就为了管理一个计数器… 😭

今天咱们就来拯救这些 “状态焦虑”—— 聊聊 Zustand,这个能让你摆脱冗余代码、轻松管理全局状态的神器!

image.png

前端状态管理的那些事儿 🤔

image.png

现代前端开发就像搭积木:UI 组件是一块块积木,而全局应用状态就是把积木粘起来的胶水。没有胶水,积木散一地;胶水太黏(比如状态管理太复杂),想调整积木又费劲。

以前咱们可能会用:

  • useState:管组件自己的状态还行,跨组件传值就歇菜
  • useContext + useReducer:能全局管理了,但得套一层又一层 Provider,代码像裹棉被
  • Redux:功能强但模板多,新手看了直呼 “劝退”

而 Zustand 就不一样了 —— 它轻得像羽毛(体积小),用起来像useState(hooks 风格),还能管全局状态。简直是为 React 量身定做的 “状态小管家”!

为什么是 Zustand?选它的 N 个理由 ✨

image.png

(一)Zustand 初印象

Zustand(发音类似 “Zustand”,德语里 “状态” 的意思)是个轻量级、hooks 化的 React 状态管理库。核心优势就俩字:简单

不管是中小型项目(比如个人博客、管理后台),还是大型项目(复杂表单、多页面数据共享),它都能 hold 住。新手看一眼 API,基本就能上手,这不比对着 Redux 文档发呆香?

(二)和其他库的 battle:为啥不选别人?

咱们直接上对比表,一目了然:

方案优点缺点适合场景
useState简单、原生不能跨组件共享单个组件内部状态
Context + Reducer原生支持、能全局管理需要 Provider 嵌套、性能一般小型项目简单全局状态
Redux生态完善、规范模板多、学习成本高超大型项目、需要严格规范
Zustand无需 Provider、API 简单生态不如 Redux 完善中小项目、追求开发效率

简单说:小项目如果组件少,useState足够;但只要需要跨组件共享状态(比如购物车数据、用户登录状态),Zustand 就是性价比之王!

实战开始!Zustand 基本操作(超简单,别慌) 🙌

image.png

(一)安装:一行命令搞定

先给项目装个 Zustand,打开终端输入:

npm install zustand

等几秒钟,搞定!比泡杯速溶咖啡还快 ☕

(二)创建你的第一个 Store:从计数器开始

Store 就是 “状态仓库”,所有全局状态都存在这里。咱们先从最简单的计数器开始,创建一个能存count的 Store。

// src/store/count.js
import { create } from "zustand";

// 创建Store:用create函数,返回一个自定义hook(useCounterStore)
export const useCounterStore = create((set, get) => ({
  // 1. 定义状态:初始count为0
  count: 0,

  // 2. 定义修改状态的方法:加1
  increment: () => set((state) => ({ count: state.count + 1 })),

  // 3. 定义修改状态的方法:减1
  decrement: () => set((state) => ({ count: state.count - 1 })),

  // 4. 获取当前状态的方法(可选,直接用get()获取)
  getCount: () => get().count,
}));

【代码解释】:

  • create:Zustand 的核心函数,用来创建 Store,参数是一个函数
  • set:修改状态的工具,调用后会触发组件重新渲染(类似setState
  • get:获取当前状态的工具(比如get().count就能拿到当前 count 值)
  • 最后返回的useCounterStore是个自定义 hook,之后在组件里直接用!

(三)在组件中使用 Store:让计数器跑起来

Store 创建好了,现在让组件 “连接” 它,显示 count 并能修改。

// src/components/Counter/index.jsx
import { useCounterStore } from "../../store/count";

const Counter = () => {
  // 从Store里“取”出需要的状态和方法
  const { count, increment, decrement } = useCounterStore();

  return (
    <div>
      <h1>当前计数 ❤️:{count}</h1>
      {/* 点击按钮调用increment,count会+1 */}
      <button onClick={increment}>加1</button>
      {/* 点击按钮调用decrement,count会-1 */}
      <button onClick={decrement}>减1</button>
    </div>
  );
};

export default Counter;

【使用说明】:

  1. 直接导入咱们创建的useCounterStore(不用包 Provider,爽!)

  2. 像解构对象一样拿出count(状态)和increment(方法)

  3. 按钮点击时调用方法,状态自动更新,组件自动重新渲染 —— 和用useState几乎一样!

现在把这个组件放进App.js,运行项目,点击按钮试试?是不是已经能正常计数了?

实战案例大放送:3 个场景吃透 Zustand 🚀

image.png

光会计数器还不够,咱们来三个实战场景,从简单到复杂,彻底学会用 Zustand 解决实际问题。

(一)计数器进阶:多组件共享状态

image.png

刚才的计数器只能在一个组件用?太浪费了!Zustand 的核心能力是全局共享—— 让多个组件用同一个 count。

比如在App.js里也显示 count,并且能修改:

// src/App.jsx
import { useCounterStore } from "./store/count";
import Counter from "./components/Counter";

function App() {
  // 同样导入useCounterStore,取状态和方法
  const { count, increment, decrement } = useCounterStore();

  return (
    <div>
      {/* App组件里的计数器 */}
      <h1>App里的计数 💖:{count}</h1>
      <button onClick={increment}>App加1</button>
      <button onClick={decrement}>App减1</button>

      {/* 引入之前写的Counter组件 */}
      <Counter />
    </div>
  );
}

export default App;

【效果演示】:

  • 点击 App 里的 “加 1”,App 和 Counter 组件的 count 会一起增加
  • 点击 Counter 里的 “减 1”,两个地方的 count 会一起减少

这就是全局状态的魔力!不管在哪个组件修改,所有用了这个状态的组件都会同步更新 —— 再也不用手动传 props 了 🎉

(二)TodoList:状态管理的经典场景

image.png

TodoList 需要增删改查任务,状态比较多(任务列表、操作方法),用 Zustand 管理再合适不过。

第一步:创建 TodoList 的 Store

// src/store/todos.js
import { create } from "zustand";

export const useTodosStore = create((set) => ({
  // 初始任务列表
  todos: [
    { id: 1, text: "学会Zustand", completed: false },
    { id: 2, text: "写一篇博客", completed: false },
  ],

  // 1. 添加任务
  addTodo: (text) => set((state) => ({
    todos: [
      ...state.todos, // 保留原来的任务
      // 新增任务(id用长度+1,简单处理)
      { id: state.todos.length + 1, text, completed: false }
    ]
  })),

  // 2. 删除任务(根据id)
  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id) // 过滤掉要删除的id
  })),

  // 3. 切换任务完成状态(根据id)
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  }))
}));

【Store 说明】:

  • 状态todos是任务数组,每个任务有idtext(内容)、completed(是否完成)
  • 三个方法分别对应 “增”(addTodo)、“删”(deleteTodo)、“改”(toggleTodo)
  • 都用set函数更新状态 ——Zustand 推荐用这种 “不可变更新” 的方式(不直接修改原数组,而是返回新数组)

第二步:创建 TodoList 组件

// src/components/TodoList/index.jsx
import { useTodosStore } from "../../store/todos";
import { useState } from "react"; // 用useState存输入框内容

const TodoList = () => {
  // 从Store取状态和方法
  const { todos, addTodo, deleteTodo, toggleTodo } = useTodosStore();
  // 本地状态:输入框内容(不需要全局共享的状态,还用useState)
  const [inputText, setInputText] = useState("");

  // 处理添加任务
  const handleAdd = () => {
    if (!inputText.trim()) return; // 空内容不添加
    addTodo(inputText); // 调用Store的addTodo方法
    setInputText(""); // 清空输入框
  };

  return (
    <div>
      <h1>Todo List 📝</h1>

      {/* 输入框和添加按钮 */}
      <div>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="输入任务..."
        />
        <button onClick={handleAdd}>添加任务</button>
      </div>

      {/* 任务列表 */}
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
            {/* 复选框切换完成状态 */}
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            {todo.text}
            {/* 删除按钮 */}
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

【核心亮点】:

  • 全局状态(todos)用 Zustand 管理,本地状态(inputText)还用useState—— 分工明确!
  • 添加任务时,调用addTodo传入输入框内容,Store 自动更新,列表自动刷新
  • 删除和切换状态同理,只需要调用 Store 的方法,不用关心状态怎么传

现在把TodoList组件放进App.js,试试添加、删除、勾选任务 —— 是不是已经能正常工作了?

(三)Github 仓库列表:异步请求 + 状态管理

image.png

实际项目中,我们经常需要请求接口(比如获取后端数据),这时候需要管理 “加载中”“请求失败” 等状态。Zustand 也能轻松搞定!

咱们做一个 “获取 Github 用户仓库列表” 的功能,包含:

  • 加载状态(loading)
  • 错误状态(error)
  • 仓库数据(repos)

第一步:准备接口请求(用 axios)

先装 axios:npm install axios,然后创建接口文件:

// src/api/config.js
import axios from "axios";
// 设置基础URL(Github API的基础地址)
axios.defaults.baseURL = "https://api.github.com";
export default axios;
// src/api/repo.js
import axios from "./config";

// 获取用户的仓库列表(比如获取"WildBlue58"这个用户的仓库)
export const getRepoList = async (owner) => {
  // 调用Github API:/users/{用户名}/repos
  const response = await axios.get(`/users/${owner}/repos`);
  return response.data; // 返回仓库数据
};

第二步:创建仓库列表的 Store(管理异步状态)

// src/store/repos.js
import { create } from "zustand";
import { getRepoList } from "../api/repo";

export const useReposStore = create((set) => ({
  repos: [], // 仓库列表数据
  loading: false, // 加载状态(true表示正在请求)
  error: null, // 错误信息(null表示无错误)

  // 异步获取仓库列表的方法
  fetchRepos: async (owner) => {
    // 1. 开始请求:设置loading为true,清空错误
    set({ loading: true, error: null });

    try {
      // 2. 调用接口获取数据
      const data = await getRepoList(owner);
      // 3. 请求成功:存数据,结束loading
      set({ repos: data, loading: false });
    } catch (err) {
      // 4. 请求失败:存错误信息,结束loading
      set({ error: err.message, loading: false });
    }
  },
}));

【异步状态管理要点】:

  • 除了数据(repos),还定义了loadingerror状态 —— 这三个状态会一起被管理
  • fetchRepos是异步方法(用async/await),调用接口的整个流程都在 Store 里封装
  • 请求前:loading: true(显示 “加载中”)
  • 请求成功:存数据repos: dataloading: false
  • 请求失败:存错误error: err.messageloading: false

第三步:创建仓库列表组件

// src/components/RepoList/index.jsx
import { useReposStore } from "../../store/repos";
import { useEffect } from "react";

const RepoList = () => {
  // 从Store取状态和方法
  const { repos, loading, error, fetchRepos } = useReposStore();

  // 组件挂载时,请求"WildBlue58"的仓库列表
  useEffect(() => {
    fetchRepos("WildBlue58");
  }, [fetchRepos]); // 依赖项加fetchRepos(虽然它不会变,但规范起见)

  // 加载中显示
  if (loading) return <p>加载中... 🌀</p>;
  // 出错显示
  if (error) return <p>出错了:{error} ❌</p>;

  return (
    <div>
      <h1>Github仓库列表 🐱</h1>
      <ul>
        {repos.map((repo) => (
          <li key={repo.id} style={{ margin: "10px 0" }}>
            {/* 仓库名称(链接到仓库页面) */}
            <a 
              href={repo.html_url} 
              target="_blank" 
              rel="noopener noreferrer"
              style={{ fontSize: "18px", color: "#0366d6" }}
            >
              {repo.name}
            </a>
            {/* 仓库描述 */}
            <p>{repo.description || "暂无描述"}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default RepoList;

【使用逻辑】:

  1. 组件一加载(useEffect),就调用fetchRepos请求数据

  2. 根据loading显示 “加载中”,根据error显示错误信息

  3. 请求成功后,遍历repos显示仓库列表

把这个组件放进App.js,运行后会看到:先显示 “加载中”,然后加载出仓库列表(如果网络没问题的话)。如果把用户名改成一个不存在的(比如 “abc123456789”),会显示错误信息 —— 这就是完整的异步状态管理流程!

总结:Zustand 值得拥有吗? 🎯

image.png

经过三个案例的实战,你应该能感受到 Zustand 的优点了:

  1. 简单易用:不用写 Provider,不用记复杂 API,像useState一样自然
  2. 功能全面:能管同步状态(计数器、Todo),也能管异步状态(接口请求)
  3. 灵活高效:全局状态和本地状态分工明确,性能也不错
  4. 学习成本低:新手花 1 小时就能上手,比 Redux 友好太多

当然,它也不是万能的 —— 如果你的项目需要非常复杂的中间件、时间旅行调试,Redux 生态可能更合适。但对于 90% 的前端项目(尤其是中小项目),Zustand 绝对是 “够用又省心” 的选择。

现在就去你的项目里试试吧!从一个简单的全局状态(比如用户登录状态)开始,慢慢体会 Zustand 的便捷 —— 相信我,用过之后你会回来感谢我的 😉