React 状态管理方案对比

中等 🟡React 生态
7 个标签
预计阅读时间:73 分钟
React状态管理ReduxContextZustandJotaiReact Query

React 状态管理方案对比

状态管理是 React 应用的核心问题之一,随着应用复杂度的增加,选择合适的状态管理方案变得至关重要。React 提供了从简单的 useState 到复杂的状态管理库等多种方案,开发者需要根据应用规模、团队熟悉度、性能需求等因素选择合适的方案。

什么是"状态"

在 React 里,状态(state)是指那些"会随时间变化、并且变化后需要重新渲染 UI"的数据。可以把 React 组件想象成一个纯函数:给它一份状态(输入),它就产出一份 UI(输出)。当状态变化时,React 重新调用组件函数,用新的输出去更新界面。理解这个"状态驱动 UI"的心智模型,是理解一切状态管理方案的前提。

状态并不是只有一种。真实项目里的状态大致可以分成四类,弄清它们的差异,才能选对工具:

| 状态类型 | 典型例子 | 生命周期 | 推荐工具 |

|----------|----------|----------|----------|

| 本地 UI 状态 | 输入框内容、开关、Tab 选中 | 随组件挂载/卸载 | useState / useReducer |

| 全局共享状态 | 登录用户、主题、语言 | 随应用生命周期 | Context / Zustand / Jotai |

| 服务器状态 | 列表数据、详情、分页 | 有缓存和过期概念 | React Query / SWR |

| URL 状态 | 搜索关键词、筛选、页码 | 随路由变化 | 路由参数 / 查询参数 |

一个常见的错误就是"把所有状态都塞进一个全局 store"。服务器状态(本质是远端数据的一份缓存)和 UI 状态(本质是内存里的临时变量)有完全不同的诉求,混在一起会让代码越来越难维护。

为什么状态管理如此重要

想象一个中后台系统:顶部有用户信息,侧边栏有菜单,主区域有表格,表格里有筛选、分页、批量操作,还有一堆弹窗。如果没有清晰的状态管理策略,你会遇到:

props 层层透传(prop drilling):一个 user 对象要从最外层传到十几层里面的头像组件。
状态散落各处:同一份数据在多个组件里各存一份,改了一处忘了改另一处,界面对不上。
无谓的重渲染:顶层一个 setState 导致整棵树重新渲染,页面卡顿。
难以调试:出了 bug 不知道是哪个组件、哪次更新导致的。

好的状态管理方案要解决的正是这几个问题:共享单一数据源可预测的更新精确的重渲染控制可调试性

内置状态管理方案

useState - 组件内部状态:

useState 是 React 最基础的状态管理 Hook,适用于组件内部的状态管理。useState 返回状态值和更新函数,支持惰性初始化和函数式更新。useState 的优势在于简单直观,与 React 组件生命周期紧密集成。useState 适用于表单输入、UI 状态(如模态框显示隐藏)、组件内部的计数器等场景。对于复杂的状态逻辑,可以考虑使用 useReducer 替代。

useState 有两个容易被忽视但很重要的用法:

惰性初始化:如果初始值需要复杂计算,传一个函数而不是直接传值,这个函数只在首次渲染时执行一次。
函数式更新:当新状态依赖旧状态时,用 setCount(c => c + 1) 而不是 setCount(count + 1),可以避免闭包陷阱和批处理丢更新问题。

useContext - 跨组件状态共享:

useContext 提供了一种在组件树中共享数据的方式,避免了 props 层层传递的问题。useContext 与 createContext 配合使用,Provider 提供数据,Consumer 或 useContext 消费数据。useContext 适用于主题、用户信息、语言设置等全局配置数据。需要注意的是,Context 的值变化会导致所有消费者组件重新渲染,对于频繁更新的状态可能需要优化。

关键要记住:Context 是一个"传递"工具,不是"状态管理"工具。它解决的是"怎么把数据传下去",而不是"怎么高效更新数据"。用 Context 存放频繁变化的状态(比如鼠标位置、输入框每一次按键),会导致所有消费该 Context 的组件全部重渲染,是性能杀手。

useReducer - 复杂状态逻辑:

useReducer 是 useState 的替代方案,适用于复杂的状态逻辑。useReducer 接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 函数。useReducer 的优势在于将状态更新逻辑集中管理,便于测试和调试。useReducer 适用于表单状态、多步骤流程、复杂的数据转换等场景。

一个判断标准:当你发现一个组件里有五六个 useState,而且它们之间还互相牵连(改一个要连带改另一个),就是时候换成 useReducer 了。reducer 是一个纯函数,接收旧状态和 action,返回新状态,天然可测试。

内置方案代码示例

javascriptCode
// useState 基础用法
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

// useState 惰性初始化:昂贵的初始计算只跑一次
function TodoList() {
  // ❌ 每次渲染都会执行 heavyParse
  // const [todos, setTodos] = useState(heavyParse(localStorage.getItem('todos')));

  // ✅ 只在首次渲染执行
  const [todos, setTodos] = useState(() =>
    heavyParse(localStorage.getItem('todos'))
  );

  return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

// useContext 跨组件状态共享
const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}

function Layout() {
  return (
    <div>
      <Header />
      <Main />
    </div>
  );
}

function Header() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <header className={`header-${theme}`}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

// useReducer 复杂状态管理
const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SET_ERROR':
      return { ...state, errors: { ...state.errors, [action.field]: action.error } };
    case 'RESET':
      return action.initialState;
    default:
      return state;
  }
};

function Form() {
  const initialState = { username: '', email: '', errors: {} };
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (field) => (e) => {
    dispatch({ type: 'SET_FIELD', field, value: e.target.value });
  };

  return (
    <form>
      <input
        value={state.username}
        onChange={handleChange('username')}
      />
      {state.errors.username && <span>{state.errors.username}</span>}
    </form>
  );
}

Context 性能优化:拆分与 memo

Context 最常见的坑就是"牵一发动全身"。下面演示如何通过拆分 Context 和配合 memo 来减少重渲染。

javascriptCode
// ❌ 反例:把频繁变化的值和稳定的值放同一个 Context
const AppContext = createContext(null);
function BadProvider({ children }) {
  const [user, setUser] = useState(null);      // 很少变
  const [mouse, setMouse] = useState({ x: 0, y: 0 }); // 每次移动都变
  // mouse 变化会让所有读 user 的组件也一起重渲染
  return (
    <AppContext.Provider value={{ user, setUser, mouse, setMouse }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ 正例:按更新频率拆分 Context
const UserContext = createContext(null);
const MouseContext = createContext(null);

function GoodProvider({ children }) {
  const [user, setUser] = useState(null);
  const [mouse, setMouse] = useState({ x: 0, y: 0 });

  // 用 useMemo 稳定 value 引用,避免 Provider 重渲染时误触发
  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const mouseValue = useMemo(() => ({ mouse, setMouse }), [mouse]);

  return (
    <UserContext.Provider value={userValue}>
      <MouseContext.Provider value={mouseValue}>
        {children}
      </MouseContext.Provider>
    </UserContext.Provider>
  );
}

// 只读 user 的组件不会因为鼠标移动而重渲染
const Avatar = memo(function Avatar() {
  const { user } = useContext(UserContext);
  return <img src={user?.avatar} alt="" />;
});

第三方状态管理库

Redux Toolkit - 企业级状态管理:

Redux Toolkit 是 Redux 的官方推荐工具集,简化了 Redux 的配置和使用。Redux Toolkit 提供了 createSlice(同时定义 reducer 和 actions)、createAsyncThunk(处理异步操作)、configureStore(配置 store)等工具。Redux Toolkit 内置 Immer,支持不可变数据更新。Redux Toolkit 适合大型应用、需要时间旅行调试、需要中间件扩展的场景。

javascriptCode
// Redux Toolkit 完整示例:同步 + 异步
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';

// 异步 thunk:处理数据请求
export const fetchTodos = createAsyncThunk('todos/fetch', async (userId) => {
  const res = await fetch(`/api/todos?userId=${userId}`);
  return res.json();
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: { list: [], status: 'idle', error: null },
  reducers: {
    // 得益于 Immer,可以"看似直接修改",底层仍是不可变更新
    addTodo: (state, action) => {
      state.list.push({ id: Date.now(), text: action.payload, done: false });
    },
    toggleTodo: (state, action) => {
      const todo = state.list.find(t => t.id === action.payload);
      if (todo) todo.done = !todo.done;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => { state.status = 'loading'; })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.list = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export const { addTodo, toggleTodo } = todosSlice.actions;

const store = configureStore({
  reducer: { todos: todosSlice.reducer },
});

// React 组件中使用 Redux
import { useSelector, useDispatch } from 'react-redux';

function TodoApp() {
  const { list, status } = useSelector((state) => state.todos);
  const dispatch = useDispatch();

  useEffect(() => { dispatch(fetchTodos(1)); }, [dispatch]);

  if (status === 'loading') return <div>Loading...</div>;

  return (
    <ul>
      {list.map((todo) => (
        <li key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))}>
          {todo.done ? '✓ ' : ''}{todo.text}
        </li>
      ))}
    </ul>
  );
}

Zustand - 轻量级状态管理:

Zustand 是一个极简的状态管理库,API 设计简洁,学习曲线平缓。Zustand 基于 Hooks,不需要 Provider 包裹,支持选择器优化渲染。Zustand 支持中间件(如 persist、devtools)、TypeScript 类型推断。Zustand 适合中小型应用、需要快速上手的团队。

javascriptCode
// Zustand 基础 store
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

function CounterWithZustand() {
  const { count, increment, decrement, reset } = useStore();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Zustand 选择器优化:只订阅需要的字段
function OptimizedCounter() {
  // 组件只在 count 变化时重渲染,其他字段变化不影响
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

// Zustand 中间件组合:持久化 + devtools
const useCartStore = create(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        addItem: (product) =>
          set((state) => ({ items: [...state.items, product] })),
        removeItem: (id) =>
          set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
        total: () => get().items.reduce((sum, i) => sum + i.price, 0),
      }),
      { name: 'cart-storage' } // localStorage key
    )
  )
);

Jotai - 原子化状态管理:

Jotai 采用原子化状态管理,状态可以按需订阅和更新,实现细粒度的渲染优化。Jotai 的 atom 可以派生出新的 atom,支持异步 atom。Jotai 的 API 非常简洁,适合需要精确控制渲染的场景。原子化的思路和 Recoil 类似:把全局状态拆成一个个最小单元(atom),组件用哪个就订阅哪个,天然避免了"整棵树重渲染"。

javascriptCode
import { atom, useAtom } from 'jotai';

// 基础 atom
const countAtom = atom(0);

// 派生 atom:自动依赖 countAtom
const doubleCountAtom = atom((get) => get(countAtom) * 2);

// 可写派生 atom
const decrementAtom = atom(
  (get) => get(countAtom),
  (get, set) => set(countAtom, get(countAtom) - 1)
);

// 异步 atom:直接在 atom 里发请求
const userAtom = atom(async (get) => {
  const id = get(countAtom);
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

function CounterWithJotai() {
  const [count, setCount] = useAtom(countAtom);
  const [doubleCount] = useAtom(doubleCountAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

React Query - 服务器状态管理:

React Query 专门用于管理服务器状态,提供了数据获取、缓存、轮询、乐观更新等功能。React Query 自动管理请求状态和缓存,支持后台刷新、预取、并行请求等高级特性。React Query 大大简化了 React 中的数据请求逻辑,推荐用于所有需要从服务器获取数据的场景。

javascriptCode
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function UserList() {
  const queryClient = useQueryClient();

  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then((res) => res.json()),
    staleTime: 5 * 60 * 1000, // 5 分钟内数据视为新鲜,不重新请求
  });

  // 乐观更新:先更新 UI,再请求,失败则回滚
  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }).then((r) => r.json()),
    onMutate: async (newUser) => {
      await queryClient.cancelQueries({ queryKey: ['users'] });
      const previous = queryClient.getQueryData(['users']);
      queryClient.setQueryData(['users'], (old) => [...old, newUser]);
      return { previous };
    },
    onError: (err, newUser, context) => {
      queryClient.setQueryData(['users'], context.previous); // 回滚
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      <button onClick={() => mutation.mutate({ id: Date.now(), name: 'New User' })}>
        Add User
      </button>
    </div>
  );
}

状态分层:真实项目里的组合拳

实际项目很少只用一种方案,更常见的是"分层组合":UI 状态用 useState,全局客户端状态用 Zustand,服务器状态用 React Query。下面是一个典型的组合示例。

javascriptCode
function StateManagementExample() {
  // 第一层:组件内部 UI 状态
  const [isModalOpen, setIsModalOpen] = useState(false);

  // 第二层:全局客户端状态(登录用户)
  const user = useUserStore((state) => state.user);

  // 第三层:服务器状态(根据用户拉取数据)
  const { data: posts } = useQuery({
    queryKey: ['posts', user.id],
    queryFn: () => fetchPosts(user.id),
  });

  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
      {isModalOpen && <Modal onClose={() => setIsModalOpen(false)} />}
      <PostList posts={posts} />
    </div>
  );
}

状态提升:共享状态的第一步

在引入任何库之前,React 官方推荐的第一招是"状态提升(lifting state up)":当两个兄弟组件需要共享同一份状态时,把状态移到它们最近的公共父组件里,再通过 props 往下传。

javascriptCode
// 温度换算:摄氏和华氏输入框共享同一个温度状态
function Calculator() {
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c');

  const celsius = scale === 'f' ? toCelsius(temperature) : temperature;
  const fahrenheit = scale === 'c' ? toFahrenheit(temperature) : temperature;

  return (
    <>
      <TemperatureInput
        scale="c"
        value={celsius}
        onChange={(v) => { setScale('c'); setTemperature(v); }}
      />
      <TemperatureInput
        scale="f"
        value={fahrenheit}
        onChange={(v) => { setScale('f'); setTemperature(v); }}
      />
    </>
  );
}

只有当提升的层级太深、props 透传太痛苦时,才需要升级到 Context 或全局库。不要跳过这一步直接上重型方案。

受控组件 vs 非受控组件

表单场景里,"状态由谁管"衍生出两种模式:受控组件(值由 React state 驱动)和非受控组件(值由 DOM 自己维护,用 ref 读取)。

javascriptCode
// 受控组件:每次输入都触发 setState,React 是唯一数据源
function ControlledInput() {
  const [value, setValue] = useState('');
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

// 非受控组件:DOM 自己存值,提交时才用 ref 读一次
function UncontrolledInput() {
  const inputRef = useRef(null);
  const handleSubmit = () => console.log(inputRef.current.value);
  return (
    <>
      <input ref={inputRef} defaultValue="" />
      <button onClick={handleSubmit}>提交</button>
    </>
  );
}

受控组件便于实时校验和联动,但每次按键都重渲染;非受控组件性能更好但难以实时响应。大型表单推荐用 react-hook-form,它内部大量使用非受控 + 订阅机制,兼顾性能与体验。

用表单库管理表单状态

手写表单状态(值、错误、touched、提交中)非常繁琐。react-hook-form 把这些收敛成一套 API,且默认走非受控模式,性能优异。

javascriptCode
import { useForm } from 'react-hook-form';

function SignupForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm();

  const onSubmit = async (data) => {
    await api.signup(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: '邮箱必填', pattern: /^\S+@\S+$/ })} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="password" {...register('password', { minLength: { value: 8, message: '至少 8 位' } })} />
      {errors.password && <span>{errors.password.message}</span>}

      <button disabled={isSubmitting}>注册</button>
    </form>
  );
}

把 URL 当作状态源

搜索关键词、筛选条件、当前页码这类状态,最好放到 URL 的查询参数里,而不是内存状态。这样刷新不丢、可分享、可前进后退,天然与浏览器历史集成。

javascriptCode
// Next.js App Router 中读写 URL 查询参数
'use client';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';

function SearchBox() {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();
  const q = params.get('q') ?? '';

  const onSearch = (value) => {
    const next = new URLSearchParams(params);
    next.set('q', value);
    router.push(`${pathname}?${next.toString()}`); // 状态存进 URL
  };

  return <input value={q} onChange={(e) => onSearch(e.target.value)} />;
}

状态机思维:XState 简介

当状态之间有复杂的"允许/禁止"转换规则时(比如一个订单只能 待支付 → 已支付 → 已发货,不能跳步),用状态机建模比一堆布尔值更清晰、更不易出错。XState 是最流行的 JS 状态机库。

javascriptCode
import { createMachine } from 'xstate';

const orderMachine = createMachine({
  id: 'order',
  initial: 'pending',
  states: {
    pending: { on: { PAY: 'paid' } },          // 待支付只能去已支付
    paid: { on: { SHIP: 'shipped', REFUND: 'refunded' } },
    shipped: { on: { CONFIRM: 'done' } },
    refunded: { type: 'final' },
    done: { type: 'final' },
  },
});
// 非法转换(如 pending 直接 SHIP)会被状态机自动忽略,杜绝了无效状态

状态机把"当前状态"和"允许的转换"显式建模,避免了用多个布尔标志(isLoading、isError、isSuccess)拼凑状态时容易出现的"不可能状态"(比如同时 isLoading 又 isSuccess)。

真实案例:电商购物车重构

某电商团队最初把整个应用状态(用户、商品列表、购物车、订单、UI 弹窗)全塞进一个 Redux store,代码里充斥着几百个 action type,加一个功能要改 5 个文件。页面在商品列表滚动时明显卡顿,因为购物车数量变化触发了整个列表重渲染。

重构方案按状态类型拆分:

商品列表、订单历史(服务器状态)→ 迁移到 React Query,自动缓存 + 后台刷新,删掉了约 40% 的样板代码。
购物车(全局客户端状态)→ 迁移到 Zustand,配合选择器,购物车角标只订阅数量字段。
弹窗、Tab、表单(本地 UI 状态)→ 回归 useState。

重构后效果对比:

| 指标 | 重构前 | 重构后 |

|------|--------|--------|

| 状态相关代码行数 | 约 4200 行 | 约 2100 行 |

| 商品列表滚动帧率 | 40 fps 左右 | 稳定 60 fps |

| 新增一个数据接口的改动文件数 | 5 个 | 1 个 |

| 重复请求(相同数据) | 频繁 | 命中缓存,几乎为 0 |

结论并不是"Redux 不好",而是"用错了工具"——把有缓存诉求的服务器状态硬塞进全局 store 是问题根源。

主流方案数据对比

| 方案 | 包体积(min+gzip) | 需要 Provider | 学习曲线 | 选择器优化 | 适用规模 |

|------|------------------|---------------|----------|------------|----------|

| useState/useReducer | 0 (内置) | 否 | 极低 | 不适用 | 组件级 |

| Context | 0 (内置) | 是 | 低 | 需手动拆分 | 低频全局 |

| Redux Toolkit | 约 13 KB | 是 | 中高 | 内置 | 大型 |

| Zustand | 约 1.2 KB | 否 | 低 | 内置 | 中小型 |

| Jotai | 约 3.4 KB | 可选 | 低 | 原子级 | 中型 |

| React Query | 约 13 KB | 是 | 中 | 按 queryKey | 服务器状态 |

(包体积为大致量级,随版本变化,仅供横向对比参考。)

常见坑

用 Context 存高频状态:鼠标位置、滚动位置、每次按键的输入值放 Context,会导致大面积重渲染。这类状态要么下沉到局部组件,要么用带选择器的库。
服务器状态放进全局 store:手动管理 loading/error/缓存/失效,写一大堆样板还容易出 bug。这正是 React Query/SWR 要解决的。
过早引入重型方案:小项目一上来就 Redux,样板代码淹没业务逻辑。先用内置方案,遇到瓶颈再升级。
忘记函数式更新:在异步回调或批处理中用 setCount(count + 1),拿到的是旧闭包值,导致丢更新。
Provider value 每次新建对象:value={{ user }} 每次渲染都是新引用,让所有消费者重渲染,要用 useMemo 稳定。
selector 返回新对象:Zustand/Redux 里 selector 返回 { a, b } 这样的新对象,每次都不相等,导致无效重渲染,需要用浅比较或拆分订阅。

最佳实践

1.先分类再选型:拿到需求先问"这是哪类状态",再决定用什么工具,别一把梭。
2.服务器状态交给专业工具:能用 React Query/SWR 就别手写 fetch + useState + useEffect。
3.状态尽量下沉:能放局部就不放全局,能放全局就别放服务器状态层。这叫"状态就近原则"。
4.全局状态用选择器订阅:只订阅组件真正需要的字段,把重渲染范围控制到最小。
5.保持更新的可预测性:复杂逻辑用 reducer 集中管理,纯函数便于测试和调试。
6.善用 devtools:Redux DevTools、Zustand devtools 中间件、React Query Devtools,能极大提升调试效率。

选择建议

小型应用:useState + useContext,简单直接,无需额外依赖
中型应用:Zustand 或 Jotai,轻量级,学习成本低
大型应用:Redux Toolkit,功能完整,生态成熟
服务器状态:React Query 或 SWR,专门优化的数据获取方案
混合使用:可以根据状态类型选择不同的方案,如 UI 状态用 Zustand,服务器状态用 React Query

进阶:用 useReducer + Context 手写迷你 Redux

在引入第三方库之前,其实用 React 内置能力就能搭一个"小型 Redux"。核心是把 useReducer 的 state 和 dispatch 通过两个独立 Context 传下去(拆开是为了让只 dispatch 不读 state 的组件不重渲染)。

javascriptCode
import { createContext, useContext, useReducer } from 'react';

const StateContext = createContext(null);
const DispatchContext = createContext(null);

const initialState = { user: null, cart: [], theme: 'light' };

function rootReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
      return { ...state, user: action.payload };
    case 'ADD_TO_CART':
      return { ...state, cart: [...state.cart, action.payload] };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    default:
      return state;
  }
}

export function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(rootReducer, initialState);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// dispatch 引用稳定,只 dispatch 的组件永远不会因 state 变化而重渲染
export const useAppState = () => useContext(StateContext);
export const useDispatch = () => useContext(DispatchContext);

function CartButton() {
  const dispatch = useDispatch(); // 不订阅 state
  return (
    <button onClick={() => dispatch({ type: 'ADD_TO_CART', payload: { id: 1 } })}>
      加入购物车
    </button>
  );
}

这套方案适合中小型应用,零依赖、可测试。但它没有选择器机制,任何 state 变化都会让所有读 state 的组件重渲染——这也是为什么规模上去后要换成带选择器的 Zustand 或 Redux。

Immer 与不可变更新原理

React 判断状态是否变化靠的是引用比较(Object.is),所以更新对象/数组必须返回新引用,这就是"不可变更新"。层级一深,手写扩展运算符会非常痛苦:

javascriptCode
// ❌ 深层嵌套的手动不可变更新,又长又易错
function reducer(state, action) {
  return {
    ...state,
    user: {
      ...state.user,
      address: {
        ...state.user.address,
        city: action.payload,
      },
    },
  };
}

// ✅ 用 Immer:写"可变"代码,产出不可变结果
import { produce } from 'immer';

function reducerWithImmer(state, action) {
  return produce(state, (draft) => {
    draft.user.address.city = action.payload;
  });
}

Immer 的原理是用 Proxy 代理一个 draft 草稿对象,记录你对草稿的所有修改,最后基于这些修改生成一个结构共享的新对象——没改动的部分复用旧引用,改动的路径才创建新对象。Redux Toolkit 的 createSlice 内部就默认集成了 Immer,这正是你能在 reducer 里"直接 push"的原因。

状态规范化(Normalization)

当服务器数据是"列表 + 引用"结构时(比如帖子里引用了作者),把它们扁平化成"以 id 为键的字典"能大幅简化更新逻辑,避免同一份数据存多份导致的不一致。

javascriptCode
// ❌ 嵌套结构:同一个 user 在多条 post 里重复,改名要遍历所有 post
const bad = {
  posts: [
    { id: 1, title: 'A', author: { id: 10, name: 'Tom' } },
    { id: 2, title: 'B', author: { id: 10, name: 'Tom' } },
  ],
};

// ✅ 规范化结构:user 只存一份,post 只存引用 id
const good = {
  posts: {
    byId: {
      1: { id: 1, title: 'A', authorId: 10 },
      2: { id: 2, title: 'B', authorId: 10 },
    },
    allIds: [1, 2],
  },
  users: {
    byId: { 10: { id: 10, name: 'Tom' } },
  },
};

// 改名只需一处
function renameUser(state, id, name) {
  return produce(state, (draft) => {
    draft.users.byId[id].name = name;
  });
}

Redux Toolkit 提供了 createEntityAdapter 来自动化这套规范化操作,内置 addOne / updateOne / removeOne / selectAll 等标准 CRUD 方法和高性能选择器。

派生状态 vs 冗余状态

一条重要原则:能算出来的就别存。如果某个值可以由现有状态推导,就不要把它单独存成一份状态,否则两者容易不同步。

javascriptCode
// ❌ 冗余:把 fullName 也存进状态,firstName 改了忘了同步就出 bug
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // 多余!

// ✅ 派生:渲染时直接算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`; // 永远同步

// 如果计算昂贵,用 useMemo 缓存
const expensiveDerived = useMemo(
  () => heavyCompute(list),
  [list]
);

SWR:另一个服务器状态方案

SWR(stale-while-revalidate)是 Vercel 出品的数据请求库,理念和 React Query 类似——先返回缓存(stale),再后台请求最新数据(revalidate)。它比 React Query 更轻量,API 更精简。

javascriptCode
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((r) => r.json());

function Profile({ id }) {
  const { data, error, isLoading, mutate } = useSWR(
    `/api/user/${id}`,
    fetcher,
    { revalidateOnFocus: true, dedupingInterval: 2000 }
  );

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>加载失败</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <button onClick={() => mutate()}>刷新</button>
    </div>
  );
}

React Query vs SWR 对比:

| 维度 | React Query | SWR |

|------|-------------|-----|

| 包体积 | 较大(约 13 KB) | 较小(约 4 KB) |

| Mutation 支持 | 强大(useMutation) | 需手动 mutate |

| 分页/无限滚动 | 内置 useInfiniteQuery | 有 useSWRInfinite |

| 缓存控制 | 精细(staleTime/gcTime) | 相对简单 |

| Devtools | 官方专用面板 | 社区方案 |

| 适用场景 | 复杂数据交互 | 以读为主的场景 |

持久化与 SSR 的坑

把状态持久化到 localStorage 时,在 Next.js 等 SSR 框架里要特别小心 hydration 不匹配:服务端渲染时读不到 localStorage,客户端首帧却读到了,两边 HTML 不一致会报错。

javascriptCode
// Zustand persist 在 SSR 下的安全写法
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useStore = create(
  persist(
    (set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })) }),
    {
      name: 'app-store',
      skipHydration: true, // 手动控制 hydration 时机
    }
  )
);

// 组件里在客户端挂载后再读取,避免首帧不匹配
function Counter() {
  const [mounted, setMounted] = useState(false);
  const count = useStore((s) => s.count);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null; // 首帧返回占位,避免 hydration mismatch
  return <div>{count}</div>;
}

性能测量与调试技巧

选型和优化都要靠数据说话,不要凭感觉。常用手段:

React DevTools Profiler:录制一次交互,看哪些组件重渲染了、耗时多少,定位无谓渲染。
Highlight updates:DevTools 里开启"高亮渲染",屏幕上会给每次重渲染的组件描边,一眼看出范围过大的更新。
Redux DevTools:查看每个 action 前后的 state diff,支持时间旅行回放。
React Query Devtools:可视化每个查询的缓存状态、是否 stale、请求次数。
why-did-you-render:开发期库,自动在控制台打印"某组件为什么重渲染了"。
javascriptCode
// 用 Profiler API 在代码里量化渲染耗时
import { Profiler } from 'react';

function onRender(id, phase, actualDuration) {
  console.log(`${id} [${phase}] 耗时 ${actualDuration.toFixed(2)}ms`);
}

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList />
    </Profiler>
  );
}

总结

| 关键要点 | 说明 |

|----------|------|

| 状态要分类 | UI / 全局 / 服务器 / URL 四类,各有专属工具 |

| Context 是传递不是管理 | 适合低频全局,高频状态会引发大面积重渲染 |

| 服务器状态用专业库 | React Query/SWR 处理缓存、失效、乐观更新 |

| 全局状态选轻量库 | Zustand/Jotai 无 Provider、带选择器、体积小 |

| 大型项目用 Redux Toolkit | 生态成熟、时间旅行调试、中间件丰富 |

| 状态就近原则 | 能局部就不全局,重渲染范围越小越好 |

状态管理没有银弹,核心是"用对的工具管对的状态"。理解状态的分类和各方案的取舍,比记住某个库的 API 更重要。

Zustand 进阶:中间件、slices 与选择器优化

前面已经用过 Zustand 的基础 API,这里深入它真正拉开差距的地方:中间件组合大型 store 的 slices 拆分选择器与浅比较。Zustand 的运行时体积约 1.2KB(gzip),核心就是一个发布订阅 + `useSyncExternalStore`,中间件只是对 `set/get` 的层层包装。

中间件洋葱模型

Zustand 中间件本质是 `(config) => (set, get, api) => config(wrappedSet, wrappedGet, api)`,一层套一层像洋葱。顺序很关键:`devtools` 要放最外层才能记录所有 action,`persist` 要在 `immer` 外面才能序列化到普通对象。

tsCode
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface BearState {
  bears: number;
  fish: number;
  addBear: () => void;
  eatFish: () => void;
}

// 中间件从内到外:immer -> persist -> subscribeWithSelector -> devtools
export const useBearStore = create<BearState>()(
  devtools(
    subscribeWithSelector(
      persist(
        immer((set) => ({
          bears: 0,
          fish: 10,
          // immer 中间件让你能直接"修改"草稿,底层仍是不可变更新
          addBear: () => set((s) => { s.bears += 1; }, false, 'addBear'),
          eatFish: () => set((s) => { s.fish -= 1; }, false, 'eatFish'),
        })),
        {
          name: 'bear-storage',
          partialize: (s) => ({ bears: s.bears }), // 只持久化 bears,不存 fish
          version: 2,
          migrate: (persisted, from) => {
            // 从旧版本迁移,避免用户 localStorage 里的老结构报错
            if (from < 2) return { bears: (persisted as any).bears ?? 0 };
            return persisted as any;
          },
        }
      )
    ),
    { name: 'BearStore' } // devtools 里显示的实例名
  )
);

`set` 的第二、三个参数(`false, 'addBear'`)是 devtools 专用:`false` 表示不替换整个 state(浅合并),字符串是 action 名,会显示在 Redux DevTools 时间线里。

slices 模式:拆分大型 store

当 store 超过 300 行,把它切成多个 slice,每个 slice 是一个返回部分 state 的函数,再在 `create` 里合并。这样团队可以并行维护、单独测试。

tsCode
import { create, StateCreator } from 'zustand';

interface CartSlice {
  items: string[];
  addItem: (id: string) => void;
}
interface UserSlice {
  userId: string | null;
  login: (id: string) => void;
}
type Store = CartSlice & UserSlice;

const createCartSlice: StateCreator<Store, [], [], CartSlice> = (set) => ({
  items: [],
  addItem: (id) => set((s) => ({ items: [...s.items, id] })),
});

const createUserSlice: StateCreator<Store, [], [], UserSlice> = (set) => ({
  userId: null,
  login: (id) => set({ userId: id }),
});

export const useStore = create<Store>()((...a) => ({
  ...createCartSlice(...a),
  ...createUserSlice(...a),
}));

选择器优化:useShallow 与 transient 更新

Zustand 默认用 `Object.is` 比较选择器返回值。如果选择器返回一个新对象/数组,每次都会触发重渲染。用 `useShallow` 做浅比较可以避免:

tsCode
import { useShallow } from 'zustand/react/shallow';

// 错误:每次 render 都返回新对象,永远不相等 -> 每次都重渲染
const { bears, fish } = useBearStore((s) => ({ bears: s.bears, fish: s.fish }));

// 正确:useShallow 逐字段浅比较,只有 bears/fish 真正变化才重渲染
const { bears, fish } = useBearStore(
  useShallow((s) => ({ bears: s.bears, fish: s.fish }))
);

对于"高频变化但不需要触发渲染"的场景(比如鼠标坐标、滚动位置),可以用 transient 更新:直接 `subscribe` 拿值写进 ref,绕过 React 渲染。实测一个每秒更新 60 次的坐标状态,用 transient 后组件重渲染次数从 60 次/秒 降到 0 次/秒。

tsxCode
function Cursor() {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    // subscribeWithSelector 中间件让 subscribe 支持选择器
    return useCursorStore.subscribe(
      (s) => s.position,
      (pos) => { if (ref.current) ref.current.style.transform = `translate(${pos.x}px,${pos.y}px)`; }
    );
  }, []);
  return <div ref={ref} className="cursor" />;
}

Jotai 原子模型深入

Zustand 是"自上而下"的单一 store,Jotai 是"自下而上"的原子(atom)组合。每个 atom 是最小状态单元,组件只订阅它用到的 atom,天然避免大范围重渲染。Jotai 核心约 3KB(gzip)。

派生 atom 与只读/可写 atom

tsCode
import { atom } from 'jotai';

const priceAtom = atom(100);
const countAtom = atom(2);

// 只读派生 atom:依赖变化时自动重算,带缓存
const totalAtom = atom((get) => get(priceAtom) * get(countAtom));

// 读写派生 atom:get 读,set 写回多个源 atom
const discountedAtom = atom(
  (get) => get(totalAtom) * 0.8,
  (get, set, newTotal: number) => {
    set(priceAtom, newTotal / get(countAtom) / 0.8);
  }
);

派生 atom 的重算是惰性且带记忆的:只有被组件订阅、且依赖真正变化时才重算,多个组件读同一个派生 atom 只算一次。

atomFamily 与异步 atom

`atomFamily` 用参数生成一族 atom,适合"按 id 管理列表项状态";异步 atom 直接把 Promise 当值,配合 Suspense 使用。

tsCode
import { atomFamily, atomWithStorage } from 'jotai/utils';
import { atom } from 'jotai';

// 每个 todoId 对应一个独立 atom,互不影响重渲染
const todoAtomFamily = atomFamily((id: string) =>
  atom({ id, done: false })
);

// 异步 atom:组件用 useAtom 读取时自动触发 Suspense
const userAtom = atom(async (get) => {
  const res = await fetch(`/api/user/${get(userIdAtom)}`);
  return res.json();
});

// 持久化 atom:值自动同步到 localStorage
const themeAtom = atomWithStorage('theme', 'light');

Redux Toolkit 现代写法

RTK 是官方推荐的 Redux 写法,把样板代码压缩了约 70%。核心是 `createSlice`(自动生成 action creators + reducer,内置 Immer)、`createAsyncThunk`(异步流程)、`RTK Query`(服务器状态)、`createEntityAdapter`(规范化 CRUD)、`listener middleware`(副作用)。

createSlice + createEntityAdapter

tsCode
import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit';

interface Todo { id: string; title: string; done: boolean; }

// entityAdapter 提供规范化的 { ids: [], entities: {} } 结构 + CRUD reducers
const todosAdapter = createEntityAdapter<Todo>({
  sortComparer: (a, b) => a.title.localeCompare(b.title),
});

export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
  const res = await fetch('/api/todos');
  return (await res.json()) as Todo[];
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: todosAdapter.getInitialState({ loading: false }),
  reducers: {
    // 直接"改" state,Immer 生成不可变更新
    toggle: (state, action) => {
      const t = state.entities[action.payload];
      if (t) t.done = !t.done;
    },
    addOne: todosAdapter.addOne, // 直接复用 adapter 的 reducer
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (s) => { s.loading = true; })
      .addCase(fetchTodos.fulfilled, (s, a) => {
        s.loading = false;
        todosAdapter.setAll(s, a.payload);
      });
  },
});

// adapter 自动生成记忆化的 selectors
export const { selectAll, selectById } = todosAdapter.getSelectors();
export const { toggle, addOne } = todosSlice.actions;
export default todosSlice.reducer;

RTK Query:内置的服务器状态方案

RTK Query 定位类似 React Query,但深度集成在 Redux store 里,自动生成 hooks、缓存、失效与轮询。

tsCode
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Todo'],
  endpoints: (build) => ({
    getTodos: build.query<Todo[], void>({
      query: () => 'todos',
      providesTags: ['Todo'],
    }),
    addTodo: build.mutation<Todo, Partial<Todo>>({
      query: (body) => ({ url: 'todos', method: 'POST', body }),
      invalidatesTags: ['Todo'], // 新增后自动让 getTodos 缓存失效并重取
    }),
  }),
});

export const { useGetTodosQuery, useAddTodoMutation } = api;

listener middleware:声明式副作用

RTK 内置的 listener middleware 可以替代大部分 redux-saga/redux-observable 场景,体积却小得多。

tsCode
import { createListenerMiddleware } from '@reduxjs/toolkit';

export const listenerMiddleware = createListenerMiddleware();

// 监听 login 成功,自动去拉取用户配置
listenerMiddleware.startListening({
  actionCreator: loginSuccess,
  effect: async (action, api) => {
    api.cancelActiveListeners(); // 防抖:取消上一次未完成的
    await api.delay(300);
    api.dispatch(fetchUserSettings(action.payload.userId));
  },
});

React Query 服务器状态进阶

前面介绍了 React Query 的基础用法,这里深入四个高频进阶场景:乐观更新无限滚动预取依赖查询与缓存失效。React Query 把服务器状态和客户端状态彻底分开,默认 `staleTime: 0`、`gcTime: 5min`(v5 里 `cacheTime` 更名为 `gcTime`)。

乐观更新(optimistic update)

先改本地缓存让 UI 立刻响应,失败再回滚。实测在 300ms 网络延迟下,用户感知的点赞响应从 300ms 降到 0ms。

tsxCode
const mutation = useMutation({
  mutationFn: (id: string) => api.like(id),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['post', id] }); // 取消在途请求
    const prev = queryClient.getQueryData(['post', id]);
    queryClient.setQueryData(['post', id], (old: any) => ({ ...old, liked: true }));
    return { prev }; // 返回快照,供出错回滚
  },
  onError: (_e, id, ctx) => {
    queryClient.setQueryData(['post', id], ctx?.prev); // 回滚
  },
  onSettled: (_d, _e, id) => {
    queryClient.invalidateQueries({ queryKey: ['post', id] }); // 最终以服务器为准
  },
});

无限滚动与分页

`useInfiniteQuery` 专门处理"加载更多",用 `getNextPageParam` 计算下一页游标。

tsxCode
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['feed'],
  queryFn: ({ pageParam }) => api.getFeed({ cursor: pageParam }),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});

// data.pages 是二维结构,展平后渲染
const items = data?.pages.flatMap((p) => p.items) ?? [];

预取与依赖查询

预取(prefetch)在用户点击前就把数据放进缓存,比如 hover 列表项时预取详情,点进去几乎 0 等待。

tsxCode
// hover 时预取详情,进入详情页直接命中缓存
function Row({ id }: { id: string }) {
  const qc = useQueryClient();
  const prefetch = () =>
    qc.prefetchQuery({ queryKey: ['detail', id], queryFn: () => api.getDetail(id) });
  return <li onMouseEnter={prefetch}>...</li>;
}

// 依赖查询:user 加载完才查 projects(enabled 控制)
const { data: user } = useQuery({ queryKey: ['user'], queryFn: api.getUser });
const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => api.getProjects(user!.id),
  enabled: !!user?.id, // user 就绪前不发请求
});

Context 深度优化:useContextSelector

原生 Context 有个硬伤:只要 `value` 引用变化,所有消费该 Context 的组件都会重渲染,即使它只用了 value 里的一个字段。除了前面提到的"拆分 Context""value 用 useMemo 包裹",还有一个更彻底的方案 —— `use-context-selector`。

它让消费者只订阅 value 的某个切片,实测在一个含 50 个消费者的 Context 里,改动其中一个字段,重渲染组件数从 50 降到 1。

tsxCode
import { createContext, useContextSelector } from 'use-context-selector';

const StoreContext = createContext<{ count: number; name: string }>(null!);

function Count() {
  // 只订阅 count,name 变化时本组件不重渲染
  const count = useContextSelector(StoreContext, (v) => v.count);
  return <span>{count}</span>;
}

React 官方也在推进内置的 `use` + context selector 能力,但在落地前,`use-context-selector` 仍是生产可用的补丁方案。

选型决策树与量化对比

面对这么多方案,用一棵决策树快速定位:

textCode
是服务器数据(需缓存/失效/重取)吗?
├── 是 → React Query / SWR / RTK Query
└── 否 → 需要跨组件共享吗?
        ├── 否 → useState / useReducer(就近原则)
        └── 是 → 更新频率高吗 / 需要精确重渲染吗?
                ├── 低频且简单 → Context + useReducer
                ├── 中等,想要轻量无 Provider → Zustand
                ├── 组件级细粒度、大量派生 → Jotai
                └── 大型团队、需时间旅行/中间件生态 → Redux Toolkit

各方案的量化对比(数字为大致量级,供选型参考):

| 方案 | 运行时体积(gzip) | Provider | 选择器 | 学习曲线 | 适用规模 |

|------|------------------|----------|--------|----------|----------|

| useState/useReducer | 0(内置) | 无 | 无 | 低 | 局部状态 |

| Context | 0(内置) | 需要 | 需第三方 | 低 | 低频全局 |

| Zustand | 约 1.2KB | 不需要 | 内置 | 低 | 中小到中大型 |

| Jotai | 约 3KB | 需要 | 原子天然 | 中 | 细粒度/派生多 |

| Redux Toolkit | 约 13KB | 需要 | reselect | 中高 | 大型团队 |

| React Query | 约 12KB | 需要 | select 选项 | 中 | 服务器状态 |

重渲染成本对比(同一场景实测量级)

场景:一个 500 项的列表,勾选其中一项。不同方案下"实际重渲染的组件数":

| 方案 | 重渲染组件数 | 说明 |

|------|--------------|------|

| 单一 Context 存整个列表 | 约 500 | value 变化触发全部消费者 |

| Zustand + useShallow 选择器 | 1 | 只有被勾选项重渲染 |

| Jotai atomFamily | 1 | 每项独立 atom |

| Redux + 记忆化 selector | 1~2 | selectById 精确订阅 |

结论一致:能把订阅粒度做细,就能把重渲染压到最低。这也是除 Context 外的库普遍内置选择器的根本原因。

进阶最佳实践清单

| 实践 | 说明 |

|------|------|

| 服务器状态和客户端状态分开 | 别把 React Query 的数据再 copy 进 Zustand |

| 选择器只返回原始值或用浅比较 | 返回新对象不加 useShallow 必然多余渲染 |

| store 按领域切 slice | 单文件不超过 300 行,便于并行维护 |

| 持久化用 partialize 白名单 | 只存必要字段,避免存入派生/临时状态 |

| 乐观更新必配回滚 + invalidate | onError 回滚,onSettled 以服务器为准 |

| 高频非渲染状态走 transient | 坐标/滚动用 subscribe 写 ref,绕过渲染 |

小结

深入到进阶层面,各库的差异其实收敛到同一组问题上:订阅粒度、更新的不可变性、副作用的组织方式、服务器与客户端状态的边界。选库时不必纠结"哪个更好",而应问"我的状态属于哪一类、更新有多频繁、团队要不要时间旅行调试"。把这几个问题回答清楚,工具自然浮现出来。