前端状态管理新范式:Zustand、Jotai 与 Preact Signals 深度对比

前言

在 React 生态中,状态管理一直是前端开发的核心议题。从 Redux 的"样板代码地狱"到 MobX 的响应式编程,再到 Context API 的原生方案,状态管理的演进始终围绕一个核心问题:如何在保证可维护性的同时,最小化不必要的组件重渲染?

2024-2025 年,新一代状态管理库以"轻量、直观、高性能"的姿态崛起。Zustand(npm 周下载量突破 500 万)、Jotai(原子化状态的代表)和 Preact Signals(细粒度响应式)分别代表了三种不同的设计哲学。本文将从设计理念、API 风格、性能表现和实战应用四个维度,对这三个库进行深度对比。


一、Zustand:极简主义的 Flux 方案

1.1 设计理念

Zustand 由 React Spring 团队开发,核心理念是**"用最少的 API 做最多的事"**。它保留了 Flux 的单向数据流思想,但去掉了 Redux 中繁琐的 action、reducer、dispatch 样板代码。

复制代码
import { create } from 'zustand';
​
interface TodoState {
  todos: { id: number; text: string; done: boolean }[];
  filter: 'all' | 'active' | 'done';
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  setFilter: (filter: 'all' | 'active' | 'done') => void;
}
​
const useStore = create<TodoState>((set) => ({
  todos: [],
  filter: 'all',
  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now(), text, done: false }],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((t) =>
        t.id === id ? { ...t, done: !t.done } : t
      ),
    })),
  setFilter: (filter) => set({ filter }),
}));

1.2 核心特性

选择性订阅是 Zustand 最大的性能优化手段。通过 selector 函数,组件只在自己关心的状态片段变化时重渲染:

复制代码
// 只有 filter 变化时才会重渲染
const filter = useStore((state) => state.filter);
​
// 只有 active todo 数量变化时才会重渲染
const activeCount = useStore(
  (state) => state.todos.filter((t) => !t.done).length
);

中间件生态丰富,官方提供了 devtools、persist、immer、subscribeWithSelector 等中间件:

复制代码
import { persist } from 'zustand/middleware';
​
const useStore = create(
  persist<TodoState>(
    (set) => ({ /* ... */ }),
    { name: 'todo-storage' } // 自动持久化到 localStorage
  )
);

1.3 适用场景

  • 中小型项目快速开发

  • 需要从 Redux 迁移但不想重写全部逻辑

  • 需要持久化、时间旅行调试等开箱即用功能


二、Jotai:原子化状态的优雅实践

2.1 设计理念

Jotai 的灵感来自 Recoil,采用**原子化(Atomic)**状态模型。每个 atom 是独立的最小状态单元,组件通过 useAtom 订阅特定的 atom,实现天然的细粒度更新。

复制代码
import { atom, useAtom } from 'jotai';
​
const todosAtom = atom<{ id: number; text: string; done: boolean }[]>([]);
const filterAtom = atom<'all' | 'active' | 'done'>('all');
​
// 派生 atom — 自动追踪依赖
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);
  switch (filter) {
    case 'active': return todos.filter((t) => !t.done);
    case 'done': return todos.filter((t) => t.done);
    default: return todos;
  }
});

2.2 核心特性

派生 atom 是 Jotai 最强大的功能。它自动追踪 get 调用的依赖,当任何依赖 atom 变化时,派生 atom 自动重新计算:

复制代码
// 统计 atom
const statsAtom = atom((get) => {
  const todos = get(todosAtom);
  return {
    total: todos.length,
    active: todos.filter((t) => !t.done).length,
    done: todos.filter((t) => t.done).length,
  };
});
​
function Stats() {
  const [stats] = useAtom(statsAtom);
  return <span>总计: {stats.total} | 待完成: {stats.active}</span>;
}

atomFamily 处理动态列表场景,为每个实体创建独立的 atom:

复制代码
import { atomFamily } from 'jotai/utils';
​
const todoAtomFamily = atomFamily((id: number) =>
  atom({ id, text: '', done: false })
);
​
// 每个 TodoItem 只订阅自己的 atom
function TodoItem({ id }: { id: number }) {
  const [todo, setTodo] = useAtom(todoAtomFamily(id));
  // ...
}

2.3 适用场景

  • 状态之间存在复杂的派生/计算关系

  • 需要极致的渲染优化(每个 atom 独立触发更新)

  • 大型应用中状态模块需要独立维护


三、Preact Signals:细粒度响应式的降维打击

3.1 设计理念

Signals 模型由 Solid.js 首创,Preact Signals 将其引入 React 生态。核心理念是**"信号自动追踪依赖,变化时精确更新 DOM,跳过 React 渲染管线"**。

复制代码
import { signal, computed } from '@preact/signals-react';
​
const todos = signal<{ id: number; text: string; done: boolean }[]>([]);
const filter = signal<'all' | 'active' | 'done'>('all');
​
// computed 自动追踪 signal 依赖
const filteredTodos = computed(() => {
  const list = todos.value;
  const f = filter.value;
  switch (f) {
    case 'active': return list.filter((t) => !t.done);
    case 'done': return list.filter((t) => t.done);
    default: return list;
  }
});

3.2 核心特性

绕过 React 渲染是 Signals 最大的性能优势。当在 JSX 中直接使用 signal 时,信号变化会直接更新 DOM 节点,不触发组件重新渲染:

复制代码
function TodoCount() {
  // count 变化时,只有 <span> 的文本更新
  // 整个 TodoCount 组件不会重新渲染
  const count = computed(() => todos.value.filter((t) => !t.done).length);
  return <span>待完成: {count}</span>;
}

与 React 无缝集成:Preact Signals 提供了 React 兼容层,可以在现有 React 项目中直接使用:

复制代码
import { useSignal } from '@preact/signals-react';

function Counter() {
  const count = useSignal(0);
  return (
    <button onClick={() => count.value++}>
      点击了 {count} 次 {/* 直接渲染 signal,自动追踪 */}
    </button>
  );
}

3.3 适用场景

  • 高频更新场景(动画、实时数据流、游戏 UI)

  • 对渲染性能有极致要求的应用

  • 希望减少 React 渲染开销但不想重构架构


四、三者横向对比

维度 Zustand Jotai Preact Signals
API 风格 Store + Selector Atom + useAtom Signal + .value
学习曲线 低(类 Redux) 中(理解原子化) 低(直觉式)
渲染优化 Selector 订阅 原子粒度 信号级(绕过 React)
TypeScript 优秀 优秀 优秀
持久化 内置中间件 需第三方 无内置
DevTools Redux DevTools Jotai DevTools 有限
包体积 ~1KB ~3KB ~2KB
生态成熟度 ★★★★★ ★★★★ ★★★
社区活跃度 极高

五、实战:同一功能三种实现

以下用同一个 Todo 应用(添加、切换、筛选、统计)展示三种方案的代码差异。

5.1 Zustand 实现

复制代码
import { create } from 'zustand';

interface Todo { id: number; text: string; done: boolean }

interface Store {
  todos: Todo[];
  filter: 'all' | 'active' | 'done';
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  setFilter: (f: 'all' | 'active' | 'done') => void;
  getFiltered: () => Todo[];
  getStats: () => { total: number; active: number; done: number };
}

const useStore = create<Store>((set, get) => ({
  todos: [],
  filter: 'all',
  addTodo: (text) =>
    set((s) => ({ todos: [...s.todos, { id: Date.now(), text, done: false }] })),
  toggleTodo: (id) =>
    set((s) => ({ todos: s.todos.map((t) => t.id === id ? { ...t, done: !t.done } : t) })),
  setFilter: (filter) => set({ filter }),
  getFiltered: () => {
    const { todos, filter } = get();
    return filter === 'all' ? todos : todos.filter((t) => (filter === 'active' ? !t.done : t.done));
  },
  getStats: () => {
    const { todos } = get();
    return { total: todos.length, active: todos.filter((t) => !t.done).length, done: todos.filter((t) => t.done).length };
  },
}));

5.2 Jotai 实现

复制代码
import { atom } from 'jotai';

interface Todo { id: number; text: string; done: boolean }

const todosAtom = atom<Todo[]>([]);
const filterAtom = atom<'all' | 'active' | 'done'>('all');

const addTodoAtom = atom(null, (get, set, text: string) => {
  const todos = get(todosAtom);
  set(todosAtom, [...todos, { id: Date.now(), text, done: false }]);
});

const toggleTodoAtom = atom(null, (get, set, id: number) => {
  set(todosAtom, get(todosAtom).map((t) => t.id === id ? { ...t, done: !t.done } : t));
});

const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);
  return filter === 'all' ? todos : todos.filter((t) => (filter === 'active' ? !t.done : t.done));
});

const statsAtom = atom((get) => {
  const todos = get(todosAtom);
  return { total: todos.length, active: todos.filter((t) => !t.done).length, done: todos.filter((t) => t.done).length };
});

5.3 Preact Signals 实现

复制代码
import { signal, computed } from '@preact/signals-react';

interface Todo { id: number; text: string; done: boolean }

const todos = signal<Todo[]>([]);
const filter = signal<'all' | 'active' | 'done'>('all');

const addTodo = (text: string) => {
  todos.value = [...todos.value, { id: Date.now(), text, done: false }];
};

const toggleTodo = (id: number) => {
  todos.value = todos.value.map((t) => t.id === id ? { ...t, done: !t.done } : t);
};

const filteredTodos = computed(() => {
  const f = filter.value;
  return f === 'all' ? todos.value : todos.value.filter((t) => (f === 'active' ? !t.done : t.done));
});

const stats = computed(() => ({
  total: todos.value.length,
  active: todos.value.filter((t) => !t.done).length,
  done: todos.value.filter((t) => t.done).length,
}));

六、选型建议

按项目规模

项目类型 推荐方案 理由
个人项目 / MVP Zustand API 最简单,5 分钟上手
中型团队项目 Zustand / Jotai Zustand 适合习惯 Redux 的团队;Jotai 适合追求优雅架构的团队
大型复杂应用 Jotai 原子化拆分天然适合模块化,派生 atom 处理复杂计算关系
高性能实时应用 Preact Signals 绕过 React 渲染管线,适合高频数据更新

按团队背景

  • Redux 老团队 → Zustand(迁移成本最低)

  • Recoil / 函数式编程爱好者 → Jotai(原子化思维一致)

  • Solid.js / Vue 转 React → Preact Signals(响应式思维一致)


总结

2025 年的前端状态管理已经告别了"唯一正确答案"的时代。Zustand 以极简 API 和成熟生态成为最稳妥的选择;Jotai 以原子化模型和派生计算提供了最优雅的架构方案;Preact Signals 以细粒度响应式带来了最极致的性能表现。

实际项目中,不必拘泥于单一方案。Zustand 管理全局状态 + Signals 处理高频局部更新,或者 Jotai 管理业务状态 + Zustand 做持久化层,都是可行的混合架构。关键是根据团队技术栈、项目规模和性能需求做出务实的选择。

推荐延伸阅读:Zustand 官方文档(zustand-demo.pmnd.rs)、Jotai 源码解析(github.com/pmndrs/jotai)、Signals 规范提案(github.com/tc39/proposal-signals)

相关推荐
布局呆星1 小时前
Vue Router 笔记(二):正则路由、组件通信与动态路由
前端·javascript·vue.js
丑八怪大丑1 小时前
HTML&CSS
前端·css·html
团象科技1 小时前
全渠道出海布局之下,多币种云结算承担着怎样的作用
前端·人工智能
lolo大魔王1 小时前
Go 语言 Web 框架 Gin 入门详解
前端·golang·gin
喵个咪2 小时前
一套Schema,生成全部代码|Kratos高效开发新范式
前端·后端·架构
Dewyze同学2 小时前
我用 Cursor 三天从零到可上线:uni-app + Fastify 全栈小程序复盘
前端
qq_381338502 小时前
前端虚拟列表与无限滚动性能优化实战:从万级数据到丝滑体验
前端·javascript·html·优化
hexu_blog2 小时前
前端vue后端springboot如何实现图片格式转换
前端·javascript·vue.js
代码煮茶2 小时前
Vue3 项目规范实战 | ESLint+Prettier+Git Hooks 搭建前端代码规范体系
前端·javascript·vue.js