React状态管理指南

790 阅读35分钟

文章顶部.png 作者卡片

React状态管理指南

引言

在React开发中状态管理一直是开发者面临的核心挑战之一。你是否遇到过这些困惑:

  • 什么时候该用useState,什么时候该用useReducer?

  • Context会导致性能问题吗?什么场景下应该使用?

  • 面对Redux、Zustand、Jotai、Recoil等众多状态库,该如何选择?

  • 服务器数据应该用React Query还是SWR?

  • 如何避免"prop drilling"(属性透传地狱)?

2025年,React生态的状态管理方案已经相当成熟,但选择的多样性也带来了决策的困难。本指南将系统地梳理React状态管理的核心概念、最佳实践,以及不同场景下的技术选型,帮助你建立清晰的状态管理思维模型。

一、理解React状态

1.1 什么是状态?

在 React 中我们可以用一个公式来描述状态和 UI 的关系,即:UI = f(状态)

React 的一个核心概念是幂等性,也就是说在 React Component 中,相同的输入(propsstatecontext)总会得到相同的输出,那我们可以用一个公式来表达:UI = f(props, state, context)

因此在 React 中状态其实本质上只有三种:propsstatecontext,即使我们日常使用的各种状态管理库背后也是使用这些。

1.2 单向数据流原则

React状态管理的基石是单向数据流——状态只能从父组件通过props传递给子组件,子组件不可直接修改父组件状态。这种设计确保数据流向可预测。

错误示例:子组件直接修改props

// ❌ 错误:子组件直接修改props
const TodoItem = ({ todo }) => {
  const handleToggle = () => {
    todo.isCompleted = !todo.isCompleted; // 直接修改导致状态不可追踪
  };
  return <input type="checkbox" checked={todo.isCompleted} onChange={handleToggle} />;
};

正确示例:通过回调函数通知父组件更新

// ✅ 正确:子组件通过回调传递状态变更请求
const TodoItem = ({ todo, onToggle }) => {
  const handleToggle = () => {
    onToggle(todo.id); // 仅传递变更标识,由父组件统一处理状态
  };
  return <input type="checkbox" checked={todo.isCompleted} onChange={handleToggle} />;
};

// 父组件中实现状态更新逻辑
const TodoList = () => {
  const [todos, setTodos] = useState([{ id: 1, title: "学习React状态管理", isCompleted: false }]);
  
  const toggleTodo = (id) => {
    setTodos(prevTodos => 
      prevTodos.map(todo => 
        todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
      )
    );
  };
  
  return todos.map(todo => <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} />);
};

1.3 状态的三大分类

根据使用范围和管理方式,状态可以划分为三类:

1. 局部状态 (Component State)

仅在单个组件内使用和维护的状态。

  • 使用场景: 表单输入、UI交互(展开/折叠)、临时计算结果

  • 管理方式useStateuseReducer

  • 特点:生命周期与组件绑定,组件销毁时状态也消失

const Counter = () => {
  const [count, setCount] = useState(0); // 局部状态
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

2. 全局状态 (Global State)

需要在多个组件间共享的状态。

  • 使用场景:用户信息、主题配置、语言设置、购物车

  • 管理方式:Context API、Redux、Zustand、Jotai等

  • 特点:独立于组件生命周期,多个组件可同时访问和修改

// 使用 Context 管理全局主题
const ThemeContext = createContext();

const App = () => {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Main />
      <Footer />
    </ThemeContext.Provider>
  );
};

3. 服务器状态 (Server State)

从服务器获取并在前端展示的数据。

  • 使用场景:API数据、数据库查询结果、实时数据流

  • 管理方式:React Query (TanStack Query)、SWR、Apollo Client

  • 特点:需要处理加载、缓存、同步、过期、重新验证等问题

// 使用 React Query 管理服务器状态
const UserProfile = ({ userId }) => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId)
  });
  
  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return <Profile user={data} />;
};

二、局部状态管理

局部状态是React中最基础也是最常用的状态类型。掌握好局部状态管理是一切的基础。

2.1 useState

基础用法

setState更新数据,reRender组件更新视图

import { useState } from 'react';

const Counter = () => {
  // 声明状态
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

复杂的状态初始化场景

使用函数式写法,这样只有第一次reRender的时候react才会去调用初始化函数,否则每次reRender都会造成无意义的函数调用,useState只会初始化一次

// ❌ 不好:每次渲染都会调用 expensiveCalculation
const [data, setData] = useState(expensiveCalculation());

// ✅ 好:只在初始化时调用一次
const [data, setData] = useState(() => expensiveCalculation());

// 实际例子
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});

上面useState例子的错误写法也只是进行了多余的计算,对功能上没有实际影响,下面来看看笔者在工作中遇到的对业务造成影响的useRef的一个例子

useRef初始化直接new BusinessPoint,useBusinessEndPoint是一个hook,会跟随使用到的组件reRender而重新执行,这样会导致有多个BusinessPoint类的实例被创建,即使useRef只会被赋值第一次创建的BusinessPoint实例,但new BusinessPoint过程中进行了postMessage事件的监听,导致消息有了多个handler,接受消息的回调函数会被执行多次,可能对业务使用造成未知的影响

const useBusinessEndPoint = (config: Config) => {
  const business = useRef<BusinessPoint>(new BusinessPoint(config));
  useEffect(() => {
    return () => {
      business.current?.destroy();
      business.current = null;
    };
  }, []);
  const sendMessage = (params?: ReqData['params']) => {
    return business.current?.sendMessage(params);
  };
  return {
    sendMessage,
  };
};

那么如何解决呢,比较尴尬的是useRef不支持useState的函数式惰性初始化的写法,所以我们需要手动实现惰性初始化

import * as React from 'react';

export function useBusinessEndPoint(config: Config) {
  const businessRef = React.useRef<BusinessPoint | null>(null);

  // ✅ 懒初始化:只在第一次 render 时创建一次
  if (businessRef.current === null) {
    businessRef.current = new BusinessPoint(config);
  }

  const sendMessage = React.useCallback((params?: ReqData['params']) => {
    return businessRef.current?.sendMessage(params);
  }, []);

  return { sendMessage };
}


// 这样把初始化过程放在render流程中可能不是太好,可以放在useEffect中

export function useBusinessEndPoint(config: Config) {
  const ref = React.useRef<BusinessPoint | null>(null);

  React.useEffect(() => {
    ref.current = new BusinessPoint(config);

    return () => {
      ref.current?.destroy();
      ref.current = null;
    };
  }, []); 
  const sendMessage = React.useCallback((params?: ReqData['params']) => {
    return ref.current?.sendMessage(params);
  }, []);

  return { sendMessage };
}


或者使用ahooks提供useCreation,专门提到解决此类问题

image.png

函数式回调更新

setState 除了直接传新值,还可以传一个函数:setCount(prev => prev + 1);

这个函数会拿到最新的上一次状态prev),并返回下一次状态。

为什么会需要它:state 更新是“排队 + 批处理”的

React 更新 state 并不保证“立刻生效”。同一轮事件里多次 setState 往往会合并/批处理,这会导致你用外层变量计算新值时,读到的是同一个旧快照

// 购物车场景,正常cartTotal的值应该基于cart数据在函数渲染过程中派生出来,但cartTotal也完全可能是一个state的情况下,比如折扣券抵扣,直接setCartTotal,就可能出现下面的情况
const addToCart = (item) => {
  // 1. 更新商品列表
  setCart([...cart, item]);

  ❌ 错误写法(基于旧的 cart,更新丢失)
  // 2. 更新总价
  setCartTotal(cart.reduce((sum, p) => sum + p.price, 0));
};

✅ 正确写法(函数式更新)

const addToCart = (item) => {
  setCart(prevCart => {
    const exists = prevCart.find(p => p.id === item.id);
    let nextCart;

    if (exists) {
      nextCart = prevCart.map(p =>
        p.id === item.id ? { ...p, quantity: p.quantity + 1 } : p
      );
    } else {
      nextCart = [...prevCart, { ...item, quantity: 1 }];
    }

    // 这里甚至可以顺手更新总价
    setCartTotal(
      nextCart.reduce((sum, p) => sum + p.price * p.quantity, 0)
    );

    return nextCart;
  });
};

上面的例子中,可能会认为可以先暂存更新后的状态,然后进行更新,看起来完全不需要函数式写法拿到最新值,这在某些前提下是可以的,但它不能替代函数式更新,原因在于:这里的 cart 仍然来自当前这次 render 的快照。如果存在“同一时间可能有别的更新也在改 cart”(并发、批处理、异步回调、快速连点、订阅消息等),这个快照就可能是旧的,从而出现“丢更新”

const addToCart = (item) => {
  // 暂存更新后的结果
  const cur = [...cart, item]
  setCart(cur);

  setCartTotal(cur.reduce((sum, p) => sum + p.price, 0));
};

什么时候这种 temp 写法是安全的?

基本要满足这些条件(越多越安全):

  • 更新只会从这个 handler 触发(不会有别的地方同时 setCart)

  • 没有异步回调里也在改 cart(比如 SSE/WebSocket/定时器/Promise)

  • 不需要承受快速连点导致的“多次 enqueue 更新”

  • 逻辑是“直接替换成一个完全由当前 cart 推导出的新数组”,且不会有并发写入

更常见的坑:异步回调里的“闭包旧值”

函数组件每次reRender都会产生一套新的变量快照。异步回调(定时器、Promise、事件监听)里引用的 state,可能永远是当时那一帧的旧值

经典的计时器的例子

useEffect(() => {
  const id = setInterval(() => {
    // 引用的count永远是第一次渲染时count的值
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);



✅ 简单写法:
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

// 当然还有很多种写法能够解决这个闭包问题

//依赖更新,创建新的定时器,这时候的闭包中就能够拿到最新的count数据了
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);



// ref配合解决,不受闭包影响,不过代码逻辑实现不够优雅,冗余出了一个违反直觉的变量
const [count, setCount] = useState(0);
const countRef = useRef(count);

useEffect(() => {
  countRef.current = count; // 每次渲染同步最新值
}, [count]);

useEffect(() => {
  const id = setInterval(() => {
    setCount(countRef.current + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

下面是一个实际业务场景中相对不易被察觉的闭包问题,使用函数式更新解决

const [state, setState] = useSetState({
  accumulatedSchema: [] as any[],
});
const { accumulatedSchema } = state;

const renderTable = async (code: string) => {
  const result = await fileStructApi({ taskId, elementModule: code });

  // ❌ accumulatedSchema 为旧快照,startMarkdownRender函数内执行循环,内层函数始终不会随着reRender而生成新的函数引用,引用的外层state也始终是第一次的
  setState({
    accumulatedSchema: [...accumulatedSchema, result],
  });
  ✅ 正确写法
  // setState((prevState) => {
  //   const newSchema = [...prevState.accumulatedSchema, result];
  //   return {
  //     accumulatedSchema: newSchema,
  //   };
  // });
};


async function startMarkdownRender(tid: string) {
  for (const code of ['A', 'B', 'C']) {
    const { done } = await fetchMarkdown(tid, code);
    if (done) {
      await renderTable(code);
    }
  }
}

useEffect(() => {
  startMarkdownRender();
},[])


多字段&大嵌套对象

多字段:一个页面/组件同时维护 loading、分页、筛选、弹窗、列表数据……很快堆成一排 useState

常见写法:一堆 useState

以“列表页”举例:筛选条件、分页、加载状态、抽屉开关、选中行、数据源。

function ListPage() {
  const [loading, setLoading] = useState(false);
  const [pageNo, setPageNo] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  const [keyword, setKeyword] = useState('');
  const [status, setStatus] = useState<'all' | 'open' | 'closed'>('all');
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
  const [dataSource, setDataSource] = useState<any[]>([]);

  const onSearch = async () => {
    setLoading(true);
    try {
      const data = await fetchList({ pageNo, pageSize, keyword, status });
      setDataSource(data);
    } finally {
      setLoading(false);
    }
  };

  const onReset = () => {
    setKeyword('');
    setStatus('all');
    setPageNo(1);
    setSelectedRowKeys([]);
  };

  // ...
}

这种写法没错,但问题是:

  • 状态分散在很多地方,重置/联动操作要写很多行

  • 很多更新是“更新一两个字段”,写起来啰嗦

  • 多个字段需要同时变更时,可读性下降(更容易漏改)

 useSetState:把同类字段收拢 + 支持“部分更新”

ahooks 的 useSetState 很适合这种“对象型 state、经常只改局部字段”的场景。

import { useSetState } from 'ahooks';

type Status = 'all' | 'open' | 'closed';

function ListPage() {
  const [state, setState] = useSetState({
    loading: false,
    pageNo: 1,
    pageSize: 20,
    keyword: '',
    status: 'all' as Status,
    drawerOpen: false,
    selectedRowKeys: [] as string[],
    dataSource: [] as any[],
  });

  const onSearch = async () => {
    setState({ loading: true });
    try {
      const data = await fetchList({
        pageNo: state.pageNo,
        pageSize: state.pageSize,
        keyword: state.keyword,
        status: state.status,
      });
      setState({ dataSource: data });
    } finally {
      setState({ loading: false });
    }
  };

  const onReset = () => {
    setState({
      keyword: '',
      status: 'all',
      pageNo: 1,
      selectedRowKeys: [],
    });
  };

  const onOpenDrawer = () => setState({ drawerOpen: true });
  const onCloseDrawer = () => setState({ drawerOpen: false });

  // ...
}

优点:

  • 同类状态集中管理,代码更聚合

  • setState({ a: 1 }) 是“局部 patch”,不用每次展开对象

  • 重置/联动特别舒服:一次 setState 改多个字段

什么时候不建议用?

  • 当某些字段更新非常频繁且彼此独立(比如高频输入 + 大对象),拆分 useState 可能更利于性能与可读性

  • 当 state 结构非常深:useSetState 的 patch 是浅合并,深层更新仍然麻烦(下一节解决)

大嵌套对象:不可变更新写法又长又容易写错

痛点例子:复杂表单草稿/配置对象

比如一个“提取配置”草稿,结构很深:

const [draft, setDraft] = useState({
  projectId: 'p1',
  rules: {
    buyer: { enabled: true, fields: { name: true, phone: false } },
    supplier: { enabled: false, fields: { company: true } },
  },
  ui: {
    drawer: { open: false, width: 520 },
  },
});

你要更新一个深层字段,比如把 buyer.fields.phone 设为 true

❌ 传统不可变更新(又长又绕)

setDraft(prev => ({
  ...prev,
  rules: {
    ...prev.rules,
    buyer: {
      ...prev.rules.buyer,
      fields: {
        ...prev.rules.buyer.fields,
        phone: true,
      },
    },
  },
}));

问题:

  • 写起来太长,业务代码被“展开语法”淹没

  • 很容易漏展开一层导致引用被复用,出现不可预期的 UI 不更新

用 Immer:像写“可变操作”,实际产生不可变新对象

Immer 的核心:给你一个 draft(草稿代理),你可以直接改它,但最终产出的是不可变的新 state。

有两种常用方式:

方式 A:直接用 immer 的 produce

import { produce } from 'immer';

setDraft(prev =>
  produce(prev, (draft) => {
    draft.rules.buyer.fields.phone = true;
  })
);

方式 B:用 useImmer(更顺手)

import { useImmer } from 'use-immer';

const [draft, updateDraft] = useImmer({
  projectId: 'p1',
  rules: {
    buyer: { enabled: true, fields: { name: true, phone: false } },
    supplier: { enabled: false, fields: { company: true } },
  },
  ui: {
    drawer: { open: false, width: 520 },
  },
});

// 更新深层字段:极其清爽
const enableBuyerPhone = () => {
  updateDraft(draft => {
    draft.rules.buyer.fields.phone = true;
  });
};

// 同时改多个深层字段也很自然
const openDrawerAndResize = () => {
  updateDraft(draft => {
    draft.ui.drawer.open = true;
    draft.ui.drawer.width = 640;
  });
};

优点:

  • 深层更新变得像“直接赋值”,可读性提升非常明显

  • 不用手动展开每一层对象

  • 同一个更新里可以改多个深层字段,代码很像业务描述

注意点:

  • Immer 适合“结构复杂的对象状态”。如果 state 非常简单(一个数字/字符串),没必要引入

  • draft 里尽量只做同步修改(不要在 producer 回调里写 await/异步)

总结:

  • 多字段但结构不深:优先 useSetState(局部 patch、重置联动舒服)

  • 结构深、嵌套多、需要频繁深层更新:用 Immer(produce/useImmer)

2.2 useReducer

useReducer 可以把它理解成:把状态更新规则集中到一个 reducer 里,用 dispatch 触发更新。它适合“状态复杂/更新逻辑多”的场景,比各处地方进行一堆 setState 更可维护。

核心概念

  • state:当前状态

  • action:一次“意图/事件”(比如 { type: 'add' }{ type: 'setKeyword', payload: 'xx' }

  • reducer(state, action):根据旧 state 和 action 计算新 state 的纯函数

  • dispatch(action):发起一次更新

一个贴合业务的例子:请求状态机

import React, { useEffect, useReducer } from 'react';

type State =
  | { status: 'idle'; data: null; error: null }
  | { status: 'loading'; data: null; error: null }
  | { status: 'success'; data: any[]; error: null }
  | { status: 'error'; data: null; error: string };

type Action =
  | { type: 'start' }
  | { type: 'success'; payload: any[] }
  | { type: 'error'; payload: string }
  | { type: 'reset' };

const initialState: State = { status: 'idle', data: null, error: null };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'start':
      return { status: 'loading', data: null, error: null };
    case 'success':
      return { status: 'success', data: action.payload, error: null };
    case 'error':
      return { status: 'error', data: null, error: action.payload };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}

// 模拟请求:1s 后随机成功/失败
function fakeFetchList(): Promise<any[]> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const ok = Math.random() > 0.3;
      if (ok) resolve([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
      else reject(new Error('Network error'));
    }, 1000);
  });
}

export default function RequestStateMachineDemo() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const load = async () => {
    dispatch({ type: 'start' });
    try {
      const data = await fakeFetchList();
      dispatch({ type: 'success', payload: data });
    } catch (e: any) {
      dispatch({ type: 'error', payload: e?.message || 'Unknown error' });
    }
  };

  useEffect(() => {
    // 首次进入自动拉一次
    load();
  }, []);

  return (
    <div style={{ fontFamily: 'sans-serif', padding: 12 }}>
      <h3>useReducer 请求状态机</h3>

      <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
        <button onClick={load} disabled={state.status === 'loading'}>
          {state.status === 'loading' ? 'Loading...' : 'Reload'}
        </button>
        <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      </div>

      <div>
        <div>
          <b>Status:</b> {state.status}
        </div>

        {state.status === 'idle' && <div>点击 Reload 发起请求</div>}

        {state.status === 'loading' && <div>正在请求中…</div>}

        {state.status === 'success' && (
          <ul>
            {state.data.map((it) => (
              <li key={it.id}>
                {it.id} - {it.name}
              </li>
            ))}
          </ul>
        )}

        {state.status === 'error' && (
          <div style={{ color: 'crimson' }}>
            <b>Error:</b> {state.error}
          </div>
        )}
      </div>
    </div>
  );
}

什么时候用 useReducer 更合适

  • 状态是对象且字段多,并且更新路径多(表单、列表页、复杂 UI)

  • 更新需要“按事件”处理:比如 FETCH_START/FETCH_SUCCESS/FETCH_ERROR

  • 想把更新逻辑集中,避免散落在多个 setXxx 里

2.3 useMemo性能优化

从官方的文档中可以看到useMemo这个hooks的定义:它在每次重新渲染的时候能够进行缓存计算的结果。useCallback是useMemo的语法糖,用来方便缓存函数引用的

image.png

主要的应用场景如下

1.跳过昂贵的重新计算

2.跳过组件的重渲染

跳过昂贵的重新计算

假设有 2 万条数据,每次渲染都要做 filter + sort(O(n log n)),如果你在组件里直接写,任何 state 改动都会重复计算。

❌ 不用 useMemo:每次 render 都重新算

❌ 不用 useMemo:每次 render 都重新算
import React, { useState } from "react";

function expensiveFilterAndSort(list, keyword) {
  // 模拟昂贵计算:filter + sort + 额外 CPU
  const t0 = performance.now();

  const result = list
    .filter((x) => x.name.toLowerCase().includes(keyword.toLowerCase()))
    .sort((a, b) => a.score - b.score);

  // 模拟额外 CPU(别在生产这么写)
  let sum = 0;
  for (let i = 0; i < 3_000_000; i++) sum += i;

  const t1 = performance.now();
  console.log("计算耗时(ms):", (t1 - t0).toFixed(1));

  return result;
}

export default function Demo({ data }) {
  const [keyword, setKeyword] = useState("");
  const [count, setCount] = useState(0);

  // 🚨 任意 state 变化都会触发昂贵计算(包括 count++)
  const visible = expensiveFilterAndSort(data, keyword);

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <button onClick={() => setCount((c) => c + 1)}>count++: {count}</button>

      <div>结果数:{visible.length}</div>
      <ul>
        {visible.slice(0, 20).map((x) => (
          <li key={x.id}>{x.name} - {x.score}</li>
        ))}
      </ul>
    </div>
  );
}

你点 count++ 会发现控制台也在打印“计算耗时”,说明它每次都在算(而这个计算跟 count 根本无关)。

✅ 用 useMemo:keyword 不变就复用上次结果

import React, { useMemo, useState } from "react";

function expensiveFilterAndSort(list, keyword) {
  const t0 = performance.now();

  const result = list
    .filter((x) => x.name.toLowerCase().includes(keyword.toLowerCase()))
    .sort((a, b) => a.score - b.score);

  let sum = 0;
  for (let i = 0; i < 3_000_000; i++) sum += i;

  const t1 = performance.now();
  console.log("计算耗时(ms):", (t1 - t0).toFixed(1));

  return result;
}

export default function Demo({ data }) {
  const [keyword, setKeyword] = useState("");
  const [count, setCount] = useState(0);

  // ✅ 只有 data / keyword 变才重算
  const visible = useMemo(() => {
    return expensiveFilterAndSort(data, keyword);
  }, [data, keyword]);

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <button onClick={() => setCount((c) => c + 1)}>count++: {count}</button>

      <div>结果数:{visible.length}</div>
      <ul>
        {visible.slice(0, 20).map((x) => (
          <li key={x.id}>{x.name} - {x.score}</li>
        ))}
      </ul>
    </div>
  );
}

效果:

  • 首次渲染:照样会算一次(所以不会提升首次渲染速度)

  • 之后你点 count++不会再打印“计算耗时”(因为 data/keyword 没变)

  • 只有你改输入框 keyword 才会重新计算

跳过组件的重渲染

useMemo缓存props的值,需要配合React.memo才能防止组件的重渲染

import React, { useCallback, useState, memo } from "react";

const ExpensiveChild = memo(function ExpensiveChild({ title, onClick }) {
  console.log("子组件渲染了");

  return (
    <div style={{ border: "1px solid #ccc", padding: 8, marginTop: 8 }}>
      <div>{title}</div>
      <button onClick={onClick}>child button</button>
    </div>
  );
});

export default function Parent() {
  const [count, setCount] = useState(0);

  // ✅ 用 useCallback 保证函数引用稳定,否则每次 render 都是新函数 -> memo 失效
  const handleChildClick = useCallback(() => {
    console.log("child click");
  }, []);

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

      <ExpensiveChild title="我不会跟着 count 重新渲染" onClick={handleChildClick} />
    </div>
  );
}

效果:

  • 点击 count++:父组件会重渲染

  • 子组件不会再打印 “子组件渲染了”(因为 title 没变,onClick 引用也没变)

常见错误处理(重点)

当组件重渲染时,对于非组件的普通元素节点都会被重渲染,无视props

const Component = () => {
  const onClick = useCallback(() => {
    /* do something */
  }, []);
  return <button onClick={onClick}>Click me</button>
};

这里子组件被memo包裹,onClick也被useCallback包裹,value并没有被包裹,这个时候,你的Component重渲染,你的MemoItem仍然会重渲染,此时useCallback还是什么都没做

const Item = () => <div> ... </div>
const MemoItem = React.memo(Item)
const Component = () => {
  const onClick = useCallback(() => {
    /* do something */
  }, []);
  return <MemoItem onClick={onClick} value={[1,2,3]}/>
};

这次看上去似乎没有问题,onClickuseCallback包裹了,然后MemoItem也被memo

const Item = () => <div> ... </div>
const MemoItem = React.memo(Item)
const Component = () => {
  const onClick = useCallback(() => {
    /* do something */
  }, []);
  return 
  <MemoItem onClick={onClick}>
    <div>something</div>
  </MemoItem>
};

然而还是会造成重渲染,因为实际也是通过props传递的

// 以下写法均等价,也就是说在props中传递children,和直接children嵌套是一致的
React.createElement('div',{
  children:'Hello World'
})

React.createElement('div',null,'Hello World')

<div>Hello World</div>
const Item = () => <div> ... </div>
const MemoItem = React.memo(Item) // useless
const Component = () => {
  const onClick = useCallback(() => { //useless
    /* do something */
  }, []);
  return 
  <MemoItem 
    onClick={onClick} 
    children={<div>something</div>}
  />
};

那么是不是把子组件也memo包裹就可以了呢

const Item = () => <div> ... </div>
const Child = () => <div>sth</div>

const MemoItem = React.memo(Item)
const MemoChild = React.memo(Child)

const Component = () => {
  const onClick = useCallback(() => { 
    /* do something */
  }, []);
  return (
    <MemoItem onClick={onClick}>
      <MemoChild />
    </MemoItem>
  )
};

答案还是没有阻止重渲染,为什么呢?来我们把MemoChild单独拿出来解析一下,它是怎么执行的:

const child = <MemoChild />;
// 只是MemoChild不变
const child = React.createElement(MemoChild,props,childen);
const child = {
  type: MemoChild,
  props: {}, // same props
  ... // same interval react stuff
}

前面的问题也迎刃而解,其实每次create的时候,React.createElement(MemoChild,props,childen);只是MemoChild被我们memo了,创建的child都是不一样的对象

如果你真的想要阻止重渲染,你应该memo的目标是Element本身,而不是ComponentuseMemo会缓存之前的值,如果memo的依赖项没有变化,则用缓存的数据返回。

const Child = () => <div>sth</div>

const MemoItem = React.memo(Item)

const Component = () => {
  const onClick = useCallback(() => { 
    /* do something */
  }, []);
  const child = useMemo(()=> <Child /> ,[])
  return (
    <MemoItem onClick={onClick}>
      {child}
    </MemoItem>
  )
};

所以实际上useMemo防止组件重渲染好用吗,你辛苦对每个属性的处理,接手项目的人只需要随手丢props,就回到了最初的起点。

你应该在所有地方加上 useMemo 吗?

总的来说,如果是基础的中后台应用,大多数交互都比较粗糙,通常不需要。如果你的应用类似图形编辑器,大多数交互是颗粒状的(即移动形状),那么此时使用Memo可能会带来很大的帮助。

使用useMemo进行优化仅在极少数情况下有价值:

  • 显然你知道这种计算非常昂贵,而且它的依赖关系很少改变。

  • 如果当前的计算结果将作为memo包裹组件的props提交。计算结果不改变,可以利用useMemo缓存结果,跳过重渲染。

  • 当前计算的结果作为某些hook的依赖项。比如其他的useMemo/useEffect依赖当前的计算结果。

三、全局状态管理

3.1 Context

使用

React Context 是我们在项目中所离不开的一种技术方案,常用来解决状态共享,特别是那些需要在多个不直接关联的组件间共享状态,以及 prop drilling(prop 下钻) 问题。

为什么要从 React Context 开始讲起呢?

诸如 React Context 与 Zustand、Jotai、Valtio 等等都是属于全局状态管理,看似它们都能解决组件之间共享状态的问题。而这些状态管理库的发明某种程度上是为了解决 React Context 的局限性以及性能问题。

在日常代码中经常可以看到数据在多个组件层级中进行传递,即数据会从顶层组件传递到深层子组件,这是一种非常不好的实践,如果你发现同事这么写代码请及时制止!例如,现在你有深度为 5 层的组件树,顶层 ComponentA维护了一个状态 count,在 ComponentE 中需要使用到这个状态,这时候通过一层层的传递来达到这个目的:

function ComponentA() {
  const count = 10;
  return <ComponentB count={count} />
}

function ComponentB({ count }) {
  return <ComponentC count={count} />
}

function ComponentC({ count }) {
  return <ComponentD count={count} />
}

function ComponentD({ count }) {
  return <ComponentE count={count} />
}

function ComponentE({ count }) {
  return <div>{count}</div>
}

image.png

可以看到,在上面的例子中,count 属性从 ComponentA -> ComponentB -> ComponentC -> ComponentD -> ComponentE 中逐层传递。这就是典型的 prop drilling(prop 下钻)。

因此,不难理解,prop drilling 会很容易引发以下几个问题。

  • 产生性能问题。当你在多个组件间传递同一个属性时,这个属性变化会导致所有组件发生 re-render,即使某些组件没有真正使用到该值,这会导致不必要的开销,从而造成性能问题

  • 减少可维护性。prop drilling 会导致你的代码可读性变差,应用变得难以维护,尤其是应用变得足够复杂时,你会发现很难增加新的功能或者改变现有的逻辑,并且容易滋生 Bug,定位和解决 Bug 也变得更加困难。

  • 增加心智负担。当跨多个组件传递同一个值时,你需要在每一个组件中添加额外的 props,即使这些组件并没有直接使用到。你会发现需要花费更多精力来追踪这个值的去向,以及这个值在哪个组件中真正被使用到,这无疑会在开发和维护中带来更多的心智负担。

而 React Context 就常用来解决这种状态共享(特别是需要在多个不直接关联的组件间共享的状态),以及 prop drilling 问题。

遗憾的是,虽然 Context 非常实用,帮助我们解决了很多问题,但是当 React Context 中任意属性发生变化时,会引起所有使用到该 Context 的组件发生 re-render,即重新渲染。但是我们希望当只有组件关心的值(或者说实际使用到的值)发生变化才会导致组件发生 re-render

关于这种重新渲染的性能问题,可以结合下面这个例子来看下,或许你在业务过程也曾遇到过

import { createContext, useContext, useState } from "react";

const context = createContext(null);

const Count1 = () => {
  const { count1, setCount1 } = useContext(context);
  console.log("Count1 render");
  return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>;
};

const Count2 = () => {
  const { count2 } = useContext(context);
  console.log("Count2 render");
  return <div>count2: {count2}</div>;
};

const StateProvider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <context.Provider
      value={{
        count1,
        count2,
        setCount1,
        setCount2
      }}
    >
      {children}
    </context.Provider>
  );
};

const App = () => (
  <StateProvider>
    <Count1 />
    <Count2 />
  </StateProvider>
);

export default App;

可以看到在 context 中包含了 count1 和 count2 以及改变它们状态的方法,在 <Count1 /> 与 <Count2 /> 组件中分别引用了 count1 和 count2,当修改 count1 状态时可以发现 <Count2 /> 组件也会发生 re-render,也就是重新渲染。很显然这里有性能上的问题,我们希望当 count1 状态发生变化时,不依赖该状态的组件不发生 re-render。

那如何解决这个问题呢?

优化

拆分 context

将 context 进行拆分,将 “大” context 拆分为多个 “小” context,这样每个组件只关心自己所用到的 context。具体逻辑如下示意图:

image.png

import { createContext, useContext, useState } from "react";

const context1 = createContext(null);
const context2 = createContext(null);

const Count1 = () => {
  const { count1, setCount1 } = useContext(context1);
  console.log("Count1 render");
  return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>;
};

const Count2 = () => {
  const { count2 } = useContext(context2);
  console.log("Count2 render");
  return <div>count2: {count2}</div>;
};

const StateProvider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  return (
    <context1.Provider
      value={{
        count1,
        setCount1
      }}
    >
      {children}
    </context1.Provider>
  );
};

const StateProvider2 = ({ children }) => {
  const [count2, setCount2] = useState(0);
  return (
    <context2.Provider
      value={{
        count2,
        setCount2
      }}
    >
      {children}
    </context2.Provider>
  );
};

const App = () => (
  <StateProvider>
    <StateProvider2>
      <Count1 />
      <Count2 />
    </StateProvider2>
  </StateProvider>
);

export default App;

借助 memo

将组件进行拆分,拆分出的子组件用 memo 包裹。

import { createContext, useContext, useState, memo } from "react";

const context = createContext(null);

const Count1 = () => {
  const { count1, setCount1 } = useContext(context);
  console.log("Count1 render");
  return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>;
};

const Count2 = memo(({ count2 }) => {
  console.log("Count2 render");
  return <div>count2: {count2}</div>;
});

const Count2Wrapper = () => {
  const { count2 } = useContext(context);
  return <Count2 count2={count2} />;
};

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <context.Provider
      value={{
        count1,
        count2,
        setCount1,
        setCount2
      }}
    >
      <Count1 />
      <Count2Wrapper />
    </context.Provider>
  );
}

借助 useMemo

在组件的 return 中,用 React.useMemo 包裹,将 Context 中消费的值,作为其依赖项。

import { createContext, useContext, useState, useMemo } from "react";

const context = createContext(null);

const Count1 = () => {
  const { count1, setCount1 } = useContext(context);
  console.log("Count1 render");
  return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>;
};

const Count2 = ({ count2 }) => {
- const { count2 } = useContext(context);
  console.log("Count2 render");
  return <div>count2: {count2}</div>;
};

+ const Count2Wrapper = () => {
+   const { count2 } = useContext(context);
+   return useMemo(() => <Count2 count2={count2} />, [count2]);
+ };

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <context.Provider
      value={{
        count1,
        count2,
        setCount1,
        setCount2
      }}
    >
      <Count1 />
      <Count2Wrapper />
    </context.Provider>
  );
}

总结

社区关于这个问题的解决方案分为了两派:

  • 不直接基于 Context 完成状态共享方案,比如我们耳熟能详的 Jotai、React Redux、Zustand 等等,这些库都不是直接基于 React Context 之上进行的改造,或者说是 React Context 的替代方案,本质上没有直接的关联,因此在状态共享的时候自然也就没有了 React Context 的性能问题。

  • 以 use-context-selector 为首的直接基于 Context 之上进行的优化:

use-context-selector 的用法非常简单,核心 API:createContext/useContextSelector 可以用来创建 context 和从 context 选取你需要的属性,如果这个属性没有发生变化则不会导致组件发生 re-render。

import React, { useState } from "react";
import { createContext, useContextSelector } from "use-context-selector";

const context = createContext(null);

const Count1 = () => {
  const { count1, setCount1 } = useContext(context);
  const count1 = useContextSelector(context, (state) => state.count1);
  const setCount1 = useContextSelector(context, (state) => state.setCount1);
  console.log("Count1 render");
  return <div onClick={() => setCount1(count1 + 1)}>count1: {count1}</div>;
};

const Count2 = () => {
  const { count2 } = useContext(context);
  const count2 = useContextSelector(context, (state) => state.count2);
  console.log("Count2 render");
  return <div>count2: {count2}</div>;
};

const StateProvider = ({ children }) => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  return (
    <context.Provider
      value={{
        count1,
        count2,
        setCount1,
        setCount2
      }}
    >
      {children}
    </context.Provider>
  );
};

const App = () => (
  <StateProvider>
    <Count1 />
    <Count2 />
  </StateProvider>
);

export default App;

3.2 状态管理库

React 第三方状态管理库对比表格

特性Redux ToolkitZustandJotaiXStateMobXValtio
学习曲线陡峭平缓平缓较陡(概念多)中等平缓
样板代码中等(已大幅减少)极少极少中等(建模为主)极少
包体积~12KB~1KB~3KB~17KB~16KB~2KB
TypeScript 支持⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
DevTools✅ 强大✅ 基础✅ 基础✅ 强(可视化)✅ 适中✅ 基础
中间件/生态✅ 丰富✅ 适中❌ 较少✅ 适中✅ 丰富⚠️ 适中
异步处理Redux Thunk / Saga内置/自定义内置/自定义内置(invoke/actor)自由(reaction/flow)自由(普通 async)
适用场景大型应用中小型应用原子化状态复杂流程/状态机复杂业务模型轻量 proxy 状态

Redux Toolkit(RTK)

核心原理 :基于Redux的官方简化方案,通过createSlice自动生成action和reducer,内置Immer支持"直接修改"状态的写法。

官方标准 Redux 写法:用 createSlice + configureStore 把 Redux 的样板代码砍掉一大半,保持 reducer/状态流可预测,并且配套 DevTools 生态成熟。

适用场景

  • 中大型应用、多人协作、需要规范(可追溯、可调试、可测试)

  • 复杂业务逻辑、跨模块共享状态很重

  • 想要成熟生态:中间件、持久化、日志、回放、时间旅行调试

优点

  • 强一致性/可预测:状态更新路径清晰(action → reducer)

  • DevTools 非常强(时间旅行、action 追踪)

  • 生态完整:异步、缓存、持久化、调试工具多

  • TS 支持好(官方模板成熟)

缺点

  • 心智模型相对重(action/reducer/selector)

  • 对简单项目偏“重武器”

  • 写法虽然简化了,但仍比 Zustand/Jotai 更“工程化”

基本使用

// store.ts
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit'

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

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Todo[],
  reducers: {
    addTodo(state, action: PayloadAction<string>) {
      state.push({ id: Date.now(), title: action.payload, done: false })
    },
    toggleTodo(state, action: PayloadAction<number>) {
      const t = state.find(x => x.id === action.payload)
      if (t) t.done = !t.done
    },
  },
})

export const { addTodo, toggleTodo } = todosSlice.actions

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

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

react中使用

// App.tsx
import React, { useState } from 'react'
import { Provider, useDispatch, useSelector } from 'react-redux'
import { store, RootState, addTodo, toggleTodo } from './store'

function Todos() {
  const [text, setText] = useState('')
  const dispatch = useDispatch()
  const todos = useSelector((s: RootState) => s.todos)

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => { dispatch(addTodo(text)); setText('') }}>add</button>

      <ul>
        {todos.map(t => (
          <li key={t.id} onClick={() => dispatch(toggleTodo(t.id))}>
            {t.done ? '✅' : '⬜️'} {t.title}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default function App() {
  return (
    <Provider store={store}>
      <Todos />
    </Provider>
  )
}

Jotai(Atom)

原子化状态:把状态拆成一个个 atom,组件只订阅自己用到的 atom;派生状态用“派生 atom”自然表达,依赖图很清晰。

适用场景

  • 中小项目、局部共享状态多

  • 状态天然可拆分(多个小状态组合、派生多)

  • 不想要 reducer 体系,但希望比 Context 更稳

优点

  • API 很小、心智轻

  • 细粒度更新:只更新用到的组件

  • 派生状态表达很顺(atom of atom / derived atom)

  • 很适合“局部全局”/组件间共享

缺点

  • 大规模工程规范需要自己定(命名、分层、atom 组织)

  • atom 太多时需要良好组织方式

  • DevTools/生态比 RTK 弱一些(但够用)

基本使用

// atoms.ts
import { atom } from 'jotai'

export const countAtom = atom(0)

export const doubleAtom = atom((get) => get(countAtom) * 2)

// 可写派生 atom(带 set)
export const incrementAtom = atom(null, (get, set) => {
  set(countAtom, get(countAtom) + 1)
})

// Counter.tsx
import React from 'react'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { countAtom, doubleAtom, incrementAtom } from './atoms'

export default function Counter() {
  const [count, setCount] = useAtom(countAtom)
  const double = useAtomValue(doubleAtom)
  const inc = useSetAtom(incrementAtom)

  return (
    <div>
      <div>count: {count}</div>
      <div>double: {double}</div>

      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={inc}>inc(atom write)</button>
    </div>
  )
}

常见坑/建议

  • atom 文件按领域拆(userAtoms/todoAtoms/uiAtoms),别全堆一个文件

  • 派生 atom 里做昂贵计算时,必要时再考虑 memo(一般够用)

zustand

特点

轻量 store + selector 订阅:一个 store 就是一个 hook,组件用 selector 取自己关心的片段,简单、直觉、性能很好。

适用场景

  • 中小应用最舒服

  • 想要“比 Redux 轻、比 Context 强”

  • UI 状态 + 一些业务状态混用也很常见

优点

  • 极简(几行就能起 store)

  • selector 精确订阅,性能好

  • 写法直觉:set/get,不需要 action/reducer

  • 生态插件常用:persist、devtools、immer

缺点

  • 约束少:团队需要自己约定 store 分层/命名

  • 复杂业务如果写成“随处 set”,可能变难维护(建议封装 actions)

  • 异步/副作用没有“官方统一范式”(但也因此自由)

基本使用

// useTodoStore.ts
import { create } from 'zustand'

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

type State = {
  todos: Todo[]
  add: (title: string) => void
  toggle: (id: number) => void
}

export const useTodoStore = create<State>((set, get) => ({
  todos: [],
  add: (title) =>
    set((s) => ({ todos: [...s.todos, { id: Date.now(), title, done: false }] })),
  toggle: (id) =>
    set((s) => ({
      todos: s.todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)),
    })),
}))

// Todos.tsx
import React, { useState } from 'react'
import { useTodoStore } from './useTodoStore'

export default function Todos() {
  const [text, setText] = useState('')
  const todos = useTodoStore(s => s.todos)      // ✅ selector
  const add = useTodoStore(s => s.add)
  const toggle = useTodoStore(s => s.toggle)

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => { add(text); setText('') }}>add</button>

      <ul>
        {todos.map(t => (
          <li key={t.id} onClick={() => toggle(t.id)}>
            {t.done ? '✅' : '⬜️'} {t.title}
          </li>
        ))}
      </ul>
    </div>
  )
}

常见坑/建议

  • selector 要稳定:避免 useStore(s => ({a:s.a})) 这种每次创建新对象导致重渲染(除非配 shallow)

  • 大型项目建议“一个领域一个 store”或“一个 store 分 slice”,别把所有东西塞一个 store

四、服务端状态管理

服务器状态与客户端状态有本质区别,需要专门的管理方案。

4.1 为什么需要专门的服务器状态管理?

传统的React应用中的数据获取依赖于useStateuseEffect钩子。这种方法最初看起来很简单,但随着应用变得更加复杂,基本限制开始显现,使这种模式越来越难以管理。

useEffect和useState对异步数据的限制

React团队官方建议在大多数情况下不要使用useEffect进行数据获取。这一建议源于几个影响应用性能和开发者体验的重要缺点。

useEffect中直接使用异步函数会立即产生问题。这个钩子期望返回的要么是空,要么是清理函数——而不是一个promise。尝试将useEffect回调标记为异步会导致意外行为,可能会扰乱React的渲染生命周期。

管理加载和错误状态需要多个必须仔细同步的状态变量:

// ❌ 问题示例:用 useState 管理服务器数据
const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);
  
  // 问题:
  // 1. 每次组件挂载都重新请求
  // 2. 没有缓存
  // 3. 其他组件无法共享这些数据
  // 4. 没有错误重试
  // 5. 没有自动刷新
  // 6. 代码重复
  
  if (loading) return <Spinner />;
  if (error) return <Error />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
};

随着数据源数量的增加,这种模式变得冗长且容易出错。当组件在异步操作完成前卸载时,经常会发生内存泄漏。没有适当的清理,在已卸载的组件上设置状态会导致运行时错误。

Effect不在服务器上运行,这使服务器端渲染变得复杂。开发者必须为服务器和客户端渲染实现不同的数据获取策略,增加了开发过程的复杂性。

常见陷阱:竞态条件重复请求 竞态条件是使用useEffect获取数据时最有问题的问题之一。当多个异步操作以意外顺序完成时,这些情况会发生,可能会显示过时或不正确的信息。

考虑一个用户快速输入的搜索功能。如果第一个搜索查询比后续查询花费更长时间解析,UI可能会显示较早查询的结果,而不是最近的查询。正如一位开发者所描述的:"你搜索Macron,然后改变主意搜索Trump,最终你得到的是你想要的**(Trump)和你得到的(Macron)**之间的不匹配"。

服务器状态在几个关键方面与客户端状态不同:

  • 外部实体可能在您的应用不知情的情况下更新它

  • 它需要显式的获取和更新操作

  • 它涉及与网络相关的问题,如加载状态、错误处理和缓存 传统的状态管理工具如ReduxContext API主要是为客户端状态设计的。它们缺乏处理服务器状态特定挑战的内置功能。

随着应用规模的扩大,使用useStateuseEffect获取服务器数据的组合变得越来越复杂。开发者必须为每个数据源手动实现加载状态、错误处理、缓存、后台更新和过时数据管理。

React Query使开发者能够编写更易于维护的代码,同时避免与传统useState/useEffect方法相关的陷阱。这种关注点分离对于构建健壮、可扩展的React应用至关重要。

4.2 React Query (TanStack Query) - 推荐方案

TanStack Query(前身为React Query)代表了React应用中管理服务器状态的专门解决方案。它的核心功能是作为异步状态管理器,而不仅仅是数据获取库。这种区别很重要,因为它塑造了我们思考和与远程数据交互的方式。

使用useQuery()进行声明式数据获取

const { data, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

这种模式抽象了复杂性,同时提供了对加载状态、错误处理和获取数据的即时访问。queryKey作为查询的唯一标识符,而queryFn包含检索数据的返回promise的函数。

这种方法的强大之处在于它如何将异步代码从命令式转变为声明式。这种转变消除了通常使用useState/useEffect模式所需的大约50%的样板代码。

内置缓存和后台更新

React Query的复杂缓存机制解决了数据管理最具挑战性的方面之一。该库根据查询键自动缓存查询结果,使对同一端点的后续调用几乎即时。

以下是生命周期的工作原理:

  1. 当组件首次使用特定键调用useQuery时,React Query获取数据并缓存它

  2. 如果另一个组件请求相同的数据,它会立即接收缓存的结果,同时在后台进行刷新

  3. 在使用查询的所有组件卸载后,缓存会在默认期限(5分钟)内持续存在,然后进行垃圾回收

React Query通过配置选项提供精细控制:

  • staleTime:确定数据何时需要刷新(默认:立即)

  • gcTime:控制未使用的数据在内存中保留多长时间(默认:5分钟)

  • refetchOnMountrefetchOnWindowFocusrefetchOnReconnect:在适当时智能刷新数据 这些功能以最少的开发者干预自动化客户端和服务器之间的数据同步。

React Query如何简化异步状态管理

服务器状态由于其异步性质和远程持久性而面临独特挑战——传统状态管理工具不是为有效处理这些特性而设计的。

React Query通过几个关键机制解决了这种复杂性:

首先,它通过基于查询键而非组件实例存储状态来消除竞态条件。这防止了过时数据覆盖较新响应的情况,这是基于useEffect方法的常见问题。

其次,它提供自动后台获取指示器。虽然status === 'loading'表示初始加载,但isFetching布尔值表示后台刷新,允许更细微的加载状态:

{isFetching ? <div>刷新中...</div> : null}

第三,React Query有效地去重多个相同的请求,即使在React的StrictMode中也能防止冗余网络调用。这种优化节省了带宽并提高了性能,特别是在较大的应用中。

最后,该库擅长通过类型级别的区分联合管理异步状态转换,确保加载、错误和成功状态保持清晰分离。

React Query将缓存转变为服务器状态的事实上的数据存储,消除了仅为处理异步数据而需要复杂状态管理架构的需求。这种范式转变使开发者能够专注于业务逻辑,而不是数据同步的复杂机制。

项目中使用

快速上手
// main.jsx - 配置
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5分钟
      cacheTime: 1000 * 60 * 10, // 10分钟
      refetchOnWindowFocus: false
    }
  }
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools initialIsOpen={false} />
  </QueryClientProvider>
);

// UserList.jsx - 使用
import { useQuery } from '@tanstack/react-query';

const fetchUsers = async () => {
  const response = await fetch('/api/users');
  if (!response.ok) throw new Error('Failed to fetch');
  return response.json();
};

const UserList = () => {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });
  
  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} onRetry={refetch} />;
  
  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
};
核心概念
  1. Query Keys - 缓存键

    // 简单的 key
    useQuery({ queryKey: ['users'], queryFn: fetchUsers });
    
    // 带参数的 key
    useQuery({ 
      queryKey: ['user', userId], 
      queryFn: () => fetchUser(userId) 
    });
    
    // 复杂的 key
    useQuery({
      queryKey: ['users', { page, filter, sort }],
      queryFn: () => fetchUsers({ page, filter, sort })
    });
    
    // Key 的作用:
    // 1. 缓存标识
    // 2. 自动重新请求(key 变化时)
    // 3. 手动失效(invalidateQueries)
    
  2. 缓存与失效策略

    import { useQuery, useQueryClient } from '@tanstack/react-query';
    
    const UserProfile = ({ userId }) => {
      const queryClient = useQueryClient();
      
      const { data } = useQuery({
        queryKey: ['user', userId],
        queryFn: () => fetchUser(userId),
        
        // 配置选项
        staleTime: 5 * 60 * 1000,     // 5分钟内不会重新请求
        cacheTime: 10 * 60 * 1000,    // 缓存保留10分钟
        retry: 3,                      // 失败重试3次
        retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
        refetchOnWindowFocus: true,    // 窗口聚焦时重新请求
        refetchOnReconnect: true,      // 重新连接时重新请求
        refetchInterval: 30000,        // 每30秒自动刷新
        enabled: !!userId              // 条件请求
      });
      
      // 手动失效缓存
      const handleUpdate = async () => {
        await updateUser(userId, newData);
        // 使该查询失效,触发重新请求
        queryClient.invalidateQueries({ queryKey: ['user', userId] });
      };
      
      return <div>{data?.name}</div>;
    };
    
  3. Mutations - 数据修改

    import { useMutation, useQueryClient } from '@tanstack/react-query';
    
    const CreateUser = () => {
      const queryClient = useQueryClient();
      
      const mutation = useMutation({
        mutationFn: (newUser) => {
          return fetch('/api/users', {
            method: 'POST',
            body: JSON.stringify(newUser)
          }).then(res => res.json());
        },
        
        // 成功后的回调
        onSuccess: (data) => {
          // 方式1: 使用户列表查询失效
          queryClient.invalidateQueries({ queryKey: ['users'] });
          
          // 方式2: 直接更新缓存
          queryClient.setQueryData(['users'], (old) => [...old, data]);
        },
        
        onError: (error) => {
          console.error('Failed to create user:', error);
        }
      });
      
      const handleSubmit = (formData) => {
        mutation.mutate(formData);
      };
      
      return (
        <form onSubmit={handleSubmit}>
          {mutation.isLoading && <Spinner />}
          {mutation.isError && <Error message={mutation.error.message} />}
          {mutation.isSuccess && <Success />}
          <button type="submit">Create</button>
        </form>
      );
    };
    
  4. 乐观更新

    const UpdateTodo = ({ todo }) => {
      const queryClient = useQueryClient();
      
      const mutation = useMutation({
        mutationFn: (updates) => updateTodo(todo.id, updates),
        
        // 乐观更新
        onMutate: async (newTodo) => {
          // 取消正在进行的查询
          await queryClient.cancelQueries({ queryKey: ['todos'] });
          
          // 保存之前的数据(用于回滚)
          const previousTodos = queryClient.getQueryData(['todos']);
          
          // 乐观地更新缓存
          queryClient.setQueryData(['todos'], (old) =>
            old.map(t => t.id === todo.id ? { ...t, ...newTodo } : t)
          );
          
          // 返回上下文(用于 onError)
          return { previousTodos };
        },
        
        // 如果失败,回滚
        onError: (err, newTodo, context) => {
          queryClient.setQueryData(['todos'], context.previousTodos);
        },
        
        // 成功后重新验证
        onSettled: () => {
          queryClient.invalidateQueries({ queryKey: ['todos'] });
        }
      });
      
      const handleToggle = () => {
        mutation.mutate({ completed: !todo.completed });
      };
      
      return (
        <input 
          type="checkbox" 
          checked={todo.completed}
          onChange={handleToggle}
        />
      );
    };
    
  5. 分页与无限滚动

// 分页
const UserList = () => {
  const [page, setPage] = useState(1);
  
  const { data, isLoading } = useQuery({
    queryKey: ['users', page],
    queryFn: () => fetchUsers(page),
    keepPreviousData: true // 保持之前的数据,避免闪烁
  });
  
  return (
    <>
      {data?.users.map(user => <UserCard key={user.id} user={user} />)}
      <button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
        Previous
      </button>
      <button onClick={() => setPage(p => p + 1)}>Next</button>
    </>
  );
};

// 无限滚动
import { useInfiniteQuery } from '@tanstack/react-query';

const InfiniteUserList = () => {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['users'],
    queryFn: ({ pageParam = 1 }) => fetchUsers(pageParam),
    getNextPageParam: (lastPage, pages) => {
      // 返回下一页的参数,或 undefined 表示没有更多
      return lastPage.hasMore ? pages.length + 1 : undefined;
    }
  });
  
  return (
    <>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.users.map(user => <UserCard key={user.id} user={user} />)}
        </div>
      ))}
      
      <button 
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    </>
  );
};

结论

传统的数据获取方法创造了不必要的复杂性。竞态条件、内存泄漏和冗长的状态管理代码困扰着仅依赖React内置钩子的应用。React Query通过其直观的useQuery和useMutation钩子以及自动工作的复杂缓存系统直接解决了这些问题