VueBloghyhero6

关于react的几个主要的hook

2026-02-28 / 2026-02-28 / 15次浏览

写在前面,一些小的项目由于项目追之过急,总之一门心思问进度,很难细品一些代码的使用场景。坐下来心境之后反而能思考,这么写代码究竟是否完全符合了react的设计意图,反之回看之前的一些代码习惯摆放位置加以总结,现在有AI生力军的加入一些有力的建议提议AI很合理的就会给指出。

好了不说了直接上代码

const MyComponent = () => {
  const [count, setCount] = useState(0);

  // 使用 useCallback 锁住函数引用
  // 只有当依赖项(比如某个搜索关键词)变化时,函数地址才会变
  const fetchData = async () => {
    const res = await fetch('/api/data');
    console.log("获取数据");
  }

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

  return (
    <div>
      <button onClick={fetchData}>手动刷新</button>
      <button onClick={() => setCount(c => c + 1)}>改变无关状态: {count}</button>
    </div>
  );
};

这段代码在能跑的情况下绝对是没有任何问题的,
它能完成初次加载获取数据,也能响应按钮点击。
但从**“代码健壮性”和“React 规范”的角度来看,这段代码其实是在“走钢丝”**。虽然现在没掉下去,但只要逻辑稍微一变,就会出 Bug。

以下是为什么要“换个思路”优化它的三个核心理由:

  1. 它是 ESLint 的“头号通缉犯”
    如果你安装了 React 官方的 Lint 插件,useEffect 那行会直接报警告。

原因:你在 useEffect 内部使用了外部定义的 fetchData,但没把它放进依赖数组 []。

后果:在一个大型团队里,这种代码通常过不了 Code Review,因为插件认为你可能遗漏了某些更新逻辑

三种更好的写法的“段位”对比

函数移入法
useEffect(() => {
  const fetchData = async () => {
    const res = await fetch('/api/data');
  };
  fetchData();
}, []); // 干净、清爽、没警告

移出组件法
如果你的请求不需要组件里的任何变量(比如 count),把它挪到组件外面去。它变成了一个静态常量,永远不会变。

const fetchData = async () => { ... }; // 放在组件外面

const MyComponent = () => {
  useEffect(() => { fetchData(); }, []); // 没问题,fetchData 是常量
  return <button onClick={fetchData}>刷新</button>;
}

useCallback 法
当你既要在 useEffect 里用,又要在 button 的 onClick 里用,且函数依赖组件内的变量时,这是唯一的解药。

const fetchData = useCallback(async () => {
  const res = await fetch(`/api/data?type=${type}`);
}, [type]); // 只有 type 变了,函数地址才变

useEffect(() => {
  fetchData();
}, [fetchData]); // 满足 ESLint 规范,且不会死循环

关于函数外挪的用法

虽然函数地址不变,但你传给函数的参数通常是会变的(比如 ID、页码、搜索关键词)。
规范的写法是:把“会变的东西”放进依赖数组,而不是函数本身。

import { fetchUserInfo } from './api'; // 外部导入,引用稳定

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 这里的 userId 是从 Props 来的,它是会变的
    fetchUserInfo(userId).then(data => setUser(data));
    
  }, [userId]); // ✅ 关键:监听 userId。当切换用户时,重新请求。

  return <div>{user?.name}</div>;
};

追加
那话说回来 究竟什么样的大型项目
会把请求放到组件里面,还要使用usecallback

场景一:复杂的“逻辑组合体” (Orchestration)
在大型项目中,一个请求往往不只是调用一个 API,它可能夹杂了大量的本地状态逻辑。

例子:带权限校验和多重过滤的搜索
假设你在做一个金融交易后台,你的请求函数 fetchData 需要实时读取:

1.当前的搜索词 keyword(State)。

2.当前的筛选条件 filters(State)。

3.用户的权限 Token(来自 Context)。

4.当前选中的账期(来自 Redux/Zustand)。

const TransactionList = () => {
  const { token } = useAuth(); // 来自全局 Context
  const { filters } = useFilterStore(); // 来自状态管理库
  const [keyword, setKeyword] = useState("");

  // 这个函数逻辑太重,且深度依赖组件内的多个 reactive 变量
  // 它没法简单地挪到组件外,因为传参会多得离谱
  const fetchData = useCallback(async () => {
    if (!token) return; // 逻辑:没登录不发请求
    
    const params = {
      q: keyword,
      ...filters,
      auth: token,
      timestamp: Date.now()
    };
    
    return api.getTransactions(params);
  }, [token, filters, keyword]); // 任何一个变了,函数引用才更新

  // 1. 自动轮询最新的交易
  useEffect(() => {
    fetchData();
    const id = setInterval(fetchData, 10000);
    return () => clearInterval(id);
  }, [fetchData]); 

  // 2. 同时这个 fetchData 还要传给搜索按钮、分页组件
  return <SearchButton onSearch={fetchData} />;
};

如果是这种聚合拼接确实要使用这种react的写法。

场景二:开发自定义 Hook (Custom Hooks)
这是大型项目中最常见的套路。为了代码复用,我们会把请求逻辑封装成一个 useApi 或者 useUser。
如果你在写一个给全公司用的 Hook,你必须用 useCallback。 因为你不知道调用者会怎么用你的函数。
如果是聚合大型项目

// 全局通用的获取用户信息的 Hook
export const useUser = (userId) => {
  const [user, setUser] = useState(null);

  // 必须用 useCallback,因为调用者可能会把它放进他自己的 useEffect 依赖里
  const refresh = useCallback(async () => {
    const data = await api.getUser(userId);
    setUser(data);
  }, [userId]);

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

  return { user, refresh }; // 把函数暴露出去给页面手动刷新
};

场景三:性能极其敏感的“长列表”或“图表”
在大型看板(Dashboard)项目中,父组件如果频繁刷新(比如有一个全局倒计时),而你的请求回调函数要传给几十个子组件(比如每一行订单都有一个“取消”或“重试”按钮)。

const OrderRow = React.memo(({ onRetry, data }) => {
  console.log("行渲染");
  return <button onClick={() => onRetry(data.id)}>重试</button>;
});

const Dashboard = () => {
  const [time, setTime] = useState(Date.now());
  
  // 每秒更新一次时间
  useEffect(() => {
    const id = setInterval(() => setTime(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);

  // 如果不用 useCallback,每秒钟所有 OrderRow 都会跟着重新渲染
  const handleRetry = useCallback((id) => {
    api.retryOrder(id);
  }, []); 

  return orders.map(order => <OrderRow key={order.id} onRetry={handleRetry} data={order} />);
};

核心疑问:id 是变的,handleRetry 怎么能不变?
这是理解 useCallback 的最高频误区。我们要区分两个概念:“函数的身份(引用)”和“函数的执行(调用)”。

打个比方:快递员与包裹
handleRetry 函数:就像是一个快递员。

id 参数:就像是快递员手里拿的包裹。

useCallback 的作用是: 保证这个“快递员”始终是同一个人,而不是每秒钟都换一个新的快递员。
至于包裹(id)是什么: 快递员(函数)在被呼叫的时候,你给他什么包裹(id),他就送什么包裹。

到最后我不知道我说清楚了没了,之前多次写过埋雷的写法,ESlint报警被我忽略掉,好在项目独立性比较高,自己维护这种写法倒是也说的过去,useCallback 和 React.memo, 用于超大型项目的应用场景似乎达不到,而且项目大量使用 Zustand 导致不了这种瓶颈。