You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用useCallback后组件仍重渲染?自定义钩子重复执行原因及解决

问题描述

我有一个用户页面,通过自定义钩子useFetchUserId()获取用户信息。该钩子使用useEffect请求数据,即便我使用了useCallback钩子,每次切换标签页时,钩子中的console.log仍会打印。请解释原因并给出修复方案?


UserProfile页面代码:

const location = useLocation();
const userId = location.state?.userId;
const [tab, setTab] = useState("1");
const { user, loading } = useFetchUserId(userId);

const handleTabChange = useCallback(function handleTabChange(event, newTab) {
  setTab(newTab);
}, []);

return (
  <TabList onChange={handleTabChange} aria-label="lab API tabs example">
    <Tab label="Info" value="1" />
    <Tab label="Cambia Password" value="2" />
    <Tab label="Curriculum" value="3" />
  </TabList>
);

useFetchUserId钩子代码:

export const useFetchUserId = (id) => {
  const [user, setUser] = useState([]);
  const [loading, setLoading] = useState(false);
  console.log("fetch  " + id);

  useEffect(() => {
    const fetch = async () => {
      try {
        const response = await axios....
        const result = response?.data;
        if (result) {
          setUser(result);
        }
      } catch (err) {
        console.log(err);
      } 
    };
    fetch();
    
  }, []);

  return { user, loading };
};

原因分析

  • 组件重渲染触发钩子执行:切换标签页时调用setTab更新了tab状态,导致UserProfile组件重渲染。自定义钩子useFetchUserId是在组件内部调用的,组件每重渲染一次,钩子的顶层代码(包括那个console.log)就会同步执行一次。useCallback只是缓存了handleTabChange函数,避免它每次重渲染都重新创建,但完全无法阻止组件本身的重渲染,自然拦不住钩子的执行。
  • 钩子顶层代码的执行逻辑:React自定义钩子本质是复用的组件逻辑,钩子的顶层代码会在每次调用它的组件重渲染时同步运行,和useEffect的异步执行逻辑无关。你把console.log放在钩子顶层,自然会随组件重渲染反复打印。
  • useEffect依赖缺失:当前useEffect的依赖数组为空,不仅会导致userId变化时无法重新请求数据,而且和本次console.log重复打印的问题无关——因为console.log不在useEffect里。

修复方案

1. 把日志移到useEffect内,仅在请求触发时打印

将console.log放到useEffect内部,这样只有当useEffect执行(即请求触发)时才会打印,同时补全useEffect的依赖项,确保userId变化时能重新请求数据:

export const useFetchUserId = (id) => {
  const [user, setUser] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    console.log("fetch  " + id); // 仅在请求触发时打印
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await axios.get(`/api/users/${id}`); // 补全实际请求地址
        const result = response?.data;
        if (result) {
          setUser(result);
        }
      } catch (err) {
        console.log(err);
      } finally {
        setLoading(false);
      }
    };
    if (id) { // 避免id为空时发起无效请求
      fetchUser();
    }
  }, [id]); // 依赖id,id变化时重新执行请求

  return { user, loading };
};

2. 可选:优化组件重渲染,减少钩子不必要的执行

如果UserProfile组件的重渲染过于频繁,可以用React.memo包裹组件,通过浅比较props避免无关状态变化导致的重渲染:

const UserProfile = React.memo(() => {
  // 原组件代码
});

注意:如果组件接收的props是复杂对象,可能需要自定义比较函数作为React.memo的第二个参数。


内容的提问来源于stack exchange,提问作者Giovanni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 22:28:38