前端状态管理新范式: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)

相关推荐
Gizzap_Tech6 小时前
国内官网增加英文版:URL、语言切换与 hreflang 怎么配置?
前端
剪刀石头布啊6 小时前
泛洪DFS、BFS
前端
qetfw6 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊6 小时前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊6 小时前
gird网格布局
前端
啃火龙果的兔子6 小时前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊7 小时前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊7 小时前
js中改变this指向的操作有哪些
前端
剪刀石头布啊7 小时前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛7 小时前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架