首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >useEffect用得越多越菜?揭秘React高手的真实秘密

useEffect用得越多越菜?揭秘React高手的真实秘密

作者头像
前端达人
发布2025-11-20 08:39:30
发布2025-11-20 08:39:30
440
举报
文章被收录于专栏:前端达人前端达人

你是否注意过一个现象:初级开发者的代码里useEffect遍地都是,而资深开发者的代码中却寥寥无几。这不是偷懒,也不是高手写得少就更高级,而是因为他们对useEffect的理解完全不在一个层次

这篇文章不聊useEffect的语法(相信你已经烂熟于心),而是从原理层面揭示:为什么你总在过度使用useEffect,而真正的高手却很少动它。

90%的开发者都理解错了useEffect的本质

大多数开发者看到useEffect,脑子里装的是这么个概念:

"需要在组件挂载时获取数据?用useEffect" "需要在某个状态改变时更新localStorage?用useEffect" "需要监听外部库的状态变化?用useEffect"

这个思维方式有个致命问题—— 你把useEffect当成了"万能事件触发器",什么事都往里装。

其实,useEffect这个名字本身就在暗示一个真理。它不叫useAfterRender,也不叫useDoSomething,而是叫useEffect

Effect的真实含义是:同步(Synchronization)

不是执行。不是触发。而是同步

高手们看到的是一道边界线

资深开发者把useEffect理解为:保持React组件与外部世界同步的工具

这个"外部"包括:网络请求、DOM操作、浏览器API、第三方库的状态……凡是超出React管控范围的东西。

反过来说,如果你在用useEffect来同步React内部的状态和状态,那就走错方向了。

看看这个现实工作中的常见场景:

代码语言:javascript
复制
// 🔴 初级做法 - 充满React内部的"同步"
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// 🟢 高手做法 - 直接计算,不需要额外状态
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`; // 渲染时直接算

看到了吗?这才是本质差异。

初级开发者创造了一个"伪状态",还需要useEffect来保持它的同步。每次依赖变化,组件就额外渲染一次,还要处理各种潜在的bug。

高手开发者明白:衍生数据根本不需要单独的状态,在渲染阶段直接计算就行

这样做的好处显而易见:

  • 没有多余的渲染周期
  • 没有状态不同步的风险
  • 代码更简洁,逻辑更清晰

这看似简单的转变,却反映了对React数据流的深刻理解。

依赖数组:这是一份合约,不是摆设

当eslint提示你"缺少依赖"时,你会怎么办?

初级开发者的两条路:

  1. 把警告的变量添加进去,然后陷入死循环,再关掉eslint规则
  2. 直接写eslint-disable-next-line react-hooks/exhaustive-deps,当什么都没发生

高手开发者会停下来问自己: 这个警告在告诉我什么?我的effect设计是不是有问题?

依赖数组不是形式要求,而是一份严肃的合约

"当这些值变化时,effect必须重新执行,因为外部世界的同步关系发生了改变"

这个合约被违反时,意味着什么?意味着你的effect在基于过期的数据运行,可能导致难以追踪的bug。

看看这个现实项目中常见的坑:

代码语言:javascript
复制
// 🔴 滥用依赖数组黑名单
useEffect(() => {
// 复杂的初始化逻辑
  initializeThirdPartyLib();
  setupEventListeners();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 真的只想运行一次吗?还是没想清楚?

// 🔴 依赖项变化导致无限循环
useEffect(() => {
  fetchData();
}, [fetchData]); // fetchData每次渲染都是新函数!

// 🟢 高手的处理方式 - 把问题解决根本
useEffect(() => {
const controller = new AbortController();

const fetch$ = fetch(`/api/data/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(err => setError(err));
    
return() => controller.abort(); // 正确的清理逻辑
}, [id]); // 只有真实变化的外部输入

// 或者,提取到自定义hook隐藏细节
function useFetchData(id) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    const controller = new AbortController();
    
    fetch(`/api/data/${id}`, { signal: controller.signal })
      .then(r => r.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
      
    return() => controller.abort();
  }, [id]);

return { data, loading, error };
}

高手的核心原则是:不要与依赖数组对抗,要理解它为什么会这样。如果依赖数组看起来很奇怪,说明effect的设计有问题,需要重构。

自定义Hook:隐藏effect的最高境界

这是区分"会用React"和"精通React"的分界线。

很多人看到这种代码就感叹"该用useEffect了":

代码语言:javascript
复制
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);

return (
    <div>
      {loading && <div>加载中...</div>}
      {error && <div>出错了:{error.message}</div>}
      {user && <div>用户:{user.name}</div>}
    </div>
  );
}

但资深开发者看到这个模式,会直接抽象成可复用的custom hook:

代码语言:javascript
复制
// 一次抽象,永久受益
function useUserData(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);

return { user, loading, error };
}

// 现在组件清爽得不行
function UserProfile({ userId }) {
const { user, loading, error } = useUserData(userId);

return (
    <div>
      {loading && <div>加载中...</div>}
      {error && <div>出错了:{error.message}</div>}
      {user && <div>用户:{user.name}</div>}
    </div>
  );
}

// 5个地方要用户数据?直接调用useUserData,不需要复制粘贴effect

这才叫抽象。这才是复用

好处远不止代码简洁:

  • 组件职责单一,只负责UI渲染
  • effect的逻辑被封装,易于维护
  • 如果api改了,只需改hook一处
  • 如果需要缓存数据,扩展hook即可

清理函数:如果你经常写它,说明设计有问题

cleanup函数的确有存在的必要——清理订阅、取消定时器、中止请求。

但这里有个关键观察:如果你的代码里cleanup函数特别多,说明你的effect在做太多事情

看这个搜索功能的常见实现:

代码语言:javascript
复制
// 🔴 什么都往一个effect里塞
useEffect(() => {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setResults);
  }, 500);

return() => {
    clearTimeout(timeoutId);
    controller.abort();
  };
}, [query]);

这个effect在做什么?防抖、延迟、fetch、中止……好几件事

高手会分解它:

代码语言:javascript
复制
// 🟢 单一职责原则应用到effect
// 第一层:防抖逻辑
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return() => clearTimeout(handler);
  }, [value, delay]);

return debouncedValue;
}

// 第二层:搜索逻辑
function useSearch(query) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }

    setLoading(true);
    const controller = new AbortController();
    
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        setResults(data);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err);
        }
        setLoading(false);
      });

    return() => controller.abort();
  }, [query]);

return { results, loading, error };
}

// 第三层:在组件中组合
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
const { results, loading, error } = useSearch(debouncedQuery);

return (
    <div>
      <input 
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      {loading && <div>搜索中...</div>}
      {error && <div>出错了</div>}
      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  );
}

关键点:每个effect只做一件事。防抖是一件事,搜索是另一件事。这样cleanup函数就清晰简单,bug也少。

这些场景根本不需要useEffect

这是最容易被忽视的判断力:什么时候不用useEffect

❌ 场景1:数据变换

代码语言:javascript
复制
// 错误做法
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() => {
  setFilteredItems(items.filter(i => i.active));
}, [items]);

// 正确做法
const [items, setItems] = useState([]);
const filteredItems = items.filter(i => i.active); // 就这么简单

❌ 场景2:事件处理

代码语言:javascript
复制
// 错误做法 - 把事件逻辑塞进effect
useEffect(() => {
  const handleClick = () => setCount(count + 1);
  button?.addEventListener('click', handleClick);
  return () => button?.removeEventListener('click', handleClick);
}, [count]);

// 正确做法 - 直接用事件处理器
const handleClick = () => setCount(count + 1);
return <button onClick={handleClick}>+1</button>;

❌ 场景3:根据props重置状态

代码语言:javascript
复制
// 错误做法 - 用effect同步
useEffect(() => {
  setFormData(initialData);
}, [initialData]);

// 正确做法 - 用key强制重新挂载
<Form key={initialData.id} initialData={initialData} />

❌ 场景4:跨组件逻辑共享

代码语言:javascript
复制
// 错误做法 - 每个组件都写一遍effect
function ComponentA() {
const [data, setData] = useState(null);
  useEffect(() => {
    // 复杂的数据处理逻辑
    processAndSetData();
  }, []);
}

function ComponentB() {
const [data, setData] = useState(null);
  useEffect(() => {
    // 同样的逻辑复制一遍
    processAndSetData();
  }, []);
}

// 正确做法 - 提取custom hook
function useProcessedData() {
const [data, setData] = useState(null);
  useEffect(() => {
    processAndSetData();
  }, []);
return data;
}

function ComponentA() {
const data = useProcessedData();
}

function ComponentB() {
const data = useProcessedData();
}

React运行模型:知道这个就能避免大部分bug

初级开发者经常被困惑:"为什么我的effect运行了两次?""为什么这个值是旧的?"

理解执行顺序就能解答一切

  1. 组件函数执行 → 返回JSX
  2. React比较新旧JSX → 决定哪些DOM需要更新
  3. 浏览器执行DOM操作 → 更新真实DOM
  4. 浏览器绘制 → 用户看到更新
  5. Effect执行 → 现在才运行你的effect代码

这意味着什么?

  • Effect永远在render之后,无法阻止渲染
  • 在Strict Mode开发环境,effect会运行两次来检测bug
  • 如果effect修改状态,会触发新的渲染循环
  • 不能在effect中依赖最新的DOM尺寸(用useLayoutEffect才行)

理解这个执行流程,你就知道为什么不能在effect里做复杂的协调逻辑。因为effect根本不是为此设计的

看看真实的高手代码长什么样

如果你看过大厂或开源库的高质量代码,会发现这个模式:

高手代码的特征:

  • ✅ 组件内useEffect极少(往往只有0-2个)
  • ✅ Effect都被包装在自定义hook里,有明确的名字
  • ✅ 每个effect只做一件事,职责单一
  • ✅ 依赖数组很自然,没有禁用规则
  • ✅ Cleanup函数简短清晰,或者根本不需要
  • ✅ 大量的数据都是render时直接计算的

初级代码的特征:

  • ❌ 组件里useEffect满天飞
  • ❌ Effect又长又复杂,试图做多件事
  • ❌ 依赖数组有警告就直接关掉eslint
  • ❌ Cleanup函数复杂且容易遗漏
  • ❌ 充满了React状态之间的"同步"

为什么差距这么大?不是因为高手知道更多api,而是他们想的更清楚

一个思维框架,助你升级useEffect水平

如果你想从滥用useEffect的陷阱里走出来,用这个框架来检视你的代码:

第一步:拷问自己

问题很简单,但答案决定了一切:

"我这个useEffect在做什么?是在同步外部状态,还是在同步内部状态?"

  • 同步外部状态(网络、localStorage、第三方库)→ 用useEffect没问题
  • 同步内部状态(React state之间) → 重新考虑设计

第二步:倾听依赖数组的声音

别和eslint对抗。如果依赖数组警告出现,说明有两种可能:

  1. 你忘记加了某个真实的依赖 → 直接加上
  2. 依赖数组看起来奇怪,说明effect设计有问题 → 重构

第三步:提取到custom hook

这是最强大的工具。任何复杂的effect都可以被提取。好处包括:

  • 隐藏实现细节
  • 增强复用性
  • 更容易测试
  • 组件职责清晰

第四步:简化effect

每个effect应该专注一件事。如果发现自己在一个effect里做多件事,就是拆分的时候了。

第五步:最重要的——知道何时不用

最好的effect是你不写的。在写useEffect之前,问问:

  • 这能在render时直接计算吗?
  • 这能用事件处理器处理吗?
  • 这能用组件的key属性处理吗?
  • 这能用custom hook且不涉及effect吗?

如果这些都不行,才是useEffect的合适场景。

最后的话

很多开发者陷入这样的怪圈:useEffect用得越多,代码越混乱,然后他们认为是自己没学透彻React,继续写更多useEffect来"修复"问题。

事实是反过来的。

真正的React高手之所以写少量useEffect,不是因为他们在"躲避"这个api,而是因为:

  1. 他们深刻理解了useEffect的真实用途(同步,不是执行)
  2. 他们懂得如何让数据流清晰(减少对effect的需求)
  3. 他们会用custom hook来隐藏复杂性(effect被隐藏在hook内部)

所以,想要真正提升React技能,不是学会更多useEffect的用法,而是学会何时不用它

这是个更高维度的思维转变。理解了这一点,你的React代码质量会有质的飞跃。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 90%的开发者都理解错了useEffect的本质
  • 高手们看到的是一道边界线
  • 依赖数组:这是一份合约,不是摆设
  • 自定义Hook:隐藏effect的最高境界
  • 清理函数:如果你经常写它,说明设计有问题
  • 这些场景根本不需要useEffect
    • ❌ 场景1:数据变换
    • ❌ 场景2:事件处理
    • ❌ 场景3:根据props重置状态
    • ❌ 场景4:跨组件逻辑共享
  • React运行模型:知道这个就能避免大部分bug
  • 看看真实的高手代码长什么样
  • 一个思维框架,助你升级useEffect水平
    • 第一步:拷问自己
    • 第二步:倾听依赖数组的声音
    • 第三步:提取到custom hook
    • 第四步:简化effect
    • 第五步:最重要的——知道何时不用
  • 最后的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档