写在前面,一些小的项目由于项目追之过急,总之一门心思问进度,很难细品一些代码的使用场景。坐下来心境之后反而能思考,这么写代码究竟是否完全符合了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。
以下是为什么要“换个思路”优化它的三个核心理由:
原因:你在 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 导致不了这种瓶颈。
文章采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。