👊👊👊领导让我从vue转到react,我敲泥*

4,461 阅读7分钟

领导让我从vue转到react,怎么办尼,那就先看关键hooks,useRffect的使用吧🍭🍭🍭

掘金上关于 useEffect 的文章不少,
但真正把它 讲清楚、讲透、讲到你能写出可维护代码 的,其实不多😉😉😉。

今天哥们直接用几个你能马上复制运行的 Demo,
从最基础的依赖数组、清理副作用、到自定义 Hook,
再扩展到 TanStack Query 为什么几乎取代 useEffect

一句话:
看完这篇,你对所有 “useEffect 什么时候写 / 写什么 / 不写会怎样” 都有清晰答案。

ALuQxX3NkvAmiwPX0Pr9ohIOPN8OGzpX.jpg


① useEffect 的本质:依赖数组才是核心

先来看最经典的例子:

useEffect(() => {
  console.log("Effect 执行,依赖 count =", count);
}, [count]);

只要你理解下面这句话,你就能掌握 useEffect:

依赖变 → 执行 effect → 执行 cleanup清理函数(如果有)

来看看 Demo:

import { useEffect, useState } from "react";

export default function DemoUseEffect() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  useEffect(() => {
    console.log("Effect 执行,依赖 count =", count);
  }, [count]);

  return (
    <div className="p-6">
      <button onClick={() => setCount(count + 1)}>count + 1</button>

      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
}

重点:

  • 点按钮 → 触发 effect
  • 输入框 → 不会触发 effect(因为 text 没写进依赖数组)

那如果依赖数组省略?useEffect(()=>{},无依赖项)

不写依赖数组,等于依赖所有 state、props → 每次渲染都会执行。

所以这是 React 社区默认“不要做”的事 ——
性能差,还容易写出无限循环。

那依赖数组写成 [] 呢?

只执行一次,之后再也不会执行。
常用于初始化逻辑。

那为什么我在控制台看到 effect 执行两次?

因为从React18+之后, React.StrictMode(开发环境)会主动触发 “mount → unmount → mount” 两次
帮你提前暴露副作用 bug。

不是你写错,是 React 故意的。

开发两次 → 线上一次

//main.tsx
 <StrictMode>
    <APP>
  </StrictMode>

② 自定义 Hook 其实就是“把 useEffect 封装起来”

你写过下面这种做“localStorage 持久化”的逻辑吗?

function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState(
    () => JSON.parse(localStorage.getItem(key) || "{}") ?? initialValue
  );

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [value]);

  return [value, setValue];
}

页面使用:

const [name, setName] = useLocalStorage("demo-name", "sss");

本质是什么?
就是:

自定义 Hook = 包装 useState / useEffect,让你的页面更干净、逻辑复用。

你返回什么,外面就用什么。
完全等于“把 effect 写内部,外面只管拿结果”。

这也是为什么:
自定义 Hook 一般都带 use 开头,并且内部可能有多个 useEffect。

自定义hooks能不能不用use开头?
任何函数内部如果调用了 useState/useEffect,就必须以 use 开头,不然就是破坏 React 的 Hook 机制。其实我自己试了,不用use开头似乎也能正常运行(😕😕😕)


③ 为什么必须写 cleanup?(比如定时器)

下面这个 Demo 很多人面试必被问:

useEffect(() => {
  const timer = setInterval(() => {
    console.log("Interval,count=", count);
  }, 1000);

  return () => {
    console.log("清理旧定时器");
    clearInterval(timer);
  };
}, [count]);

流程是这样的:

  • count 第一次是 0 → 开启一个定时器
  • 点按钮 count = 1 → 清理旧定时器 → 开一个新的
  • 再点 → 一样清理 → 重建

如果你没有写 cleanup 会怎样?

  • 每次点都会创建一个新的定时器。
  • console 会变成“机关枪模式”。
  • 性能暴涨,浏览器发热,CPU 起飞 🔥
  • React 官方把这叫做 内存泄漏(memory leak)

所以:

任何产生订阅、定时器、事件监听、外部资源的 effect,都必须写 cleanup。

这是 useEffect 的最重要规则之一。


④ 防抖 / 节流输入框:useEffect 的神级用法

防抖逻辑:

function useDebounce(value: string, delay = 800) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

使用:

const debounced = useDebounce(text, 1000);

useEffect(() => {
  if (debounced) {
    console.log("防抖触发 →", debounced);
  }
}, [debounced]);

输入快,不触发;
停止 1s,才触发。

这是 effect 的另一个核心用法:
根据业务去做一些自定义的联动工具hooks


⑤ 重点扩展:为什么 TanStack Query (tanstack.com/query/lates…) 让你越来越少写 useEffect?

React 社区现在有一句话:

“越写越多 useEffect,代码越乱。useEffect在一个页面使用太多,太多的副作用域导致代码逻辑极度混乱,数据层形成干扰”

然后大家发现了更现代的做法:
用 TanStack Query(React Query),几乎不需要手写 useEffect 来请求数据了。

传统写法:

useEffect(() => {
  axios.get("/api/user").then(res => setUser(res.data));
}, []);

有:

  • loading 状态
  • error 状态
  • 缓存策略
  • 重试逻辑
  • refetch 机制

写成地狱。

而 React Query:

const { data, isLoading, error } = useQuery({
  queryKey: ["user"],
  queryFn: fetchUser,
});

🎉 不需要 useEffect:

  • 自动请求
  • 自动缓存
  • 自动失败重试
  • 自动并发控制
  • 自动后台刷新(Stale-While-Revalidate)
  • 自动依赖感知更新

这就是为什么:

React Query 正在让 useEffect 专注于“副作用”,不再用于“业务逻辑”。

这一点对项目复杂度提升巨大。


⑥ useEffect 的黄金法则(你能把这段贴到团队规范里)

1. useEffect 只处理副作用,不要处理业务

比如请求数据 → 用 React Query
比如格式化数据 → 用 useMemo
比如事件 → 封装成自定义 Hook

2. 依赖数组永远要写全(让 ESLint 帮你)

不写 / 漏写依赖 = bug 温床。

3. 如非必要,不要写空依赖数组 []

会导致数据永不更新。

4. cleanup 是必须的(定时器、事件、订阅)

否则内存泄漏 + 性能炸裂。

5. 自定义 Hook = 复用 useEffect 的最佳方式


⑦ useEffect常见问题

  1. useLayoutEffect vs useEffect 区别

JS 运行 :
→ DOM 更新 → 样式计算 (Style) → 布局 (Layout) → 绘制 (Paint) → 合成 (Composite) → 显示到屏幕

useLayoutEffect 运行在 DOM 更新后,Layout之前同步执行(阻塞渲染,慎用)

useEffect 运行在 Paint 之后的异步阶段。

  1. useEffect(hooks) 不能写在条件里

第一次渲染:useState → useEffect → useMemo
第二次渲染:useState → useEffect → useMemo
用条件判断会导致比如
第一次渲染:useState(A) → useEffect(B) → useMemo(c)
第二次渲染:useState(A) → 无 → useMemo(c)
A B C => A C 我姑且称react不支持这样比较

React 必须保证每次渲染顺序完全一致 才能正确取到对应的 hook 状态。


  1. 为什么无限循环
  • effect 内部修改了 deps 依赖

  • deps 每次都变化 → effect 每次都运行 → 又 setState → 无限循环

写代码中,无限循环这个问题还是比较常见的,需要注意排查

⑧ 进阶死穴:当依赖项是“对象”或“数组”时

你可能会写出这样的代码:

JavaScript

const userInfo = { id: 1, name: '张三' };

useEffect(() => {
  console.log("用户信息变了,去请求数据...");
}, [userInfo]); // ❌ 这里的坑很大

1. 它是浅比较(Object.is)

React 对依赖数组的检查非常“死板”。它使用的是 浅比较(Shallow Comparison)

React 逻辑: oldDeps[i] === newDeps[i] ? 如果你每次渲染都声明一个新对象,即便内容一模一样,{} 也不等于 {}

2. 为什么会触发无限循环?

如果这个对象是在组件函数体内声明的:

  • 第 1 次渲染: 创建 userInfo(引用 A),执行 Effect。
  • 第 2 次渲染: 重新运行函数,创建全新userInfo(引用 B),React 发现 A !== B,再次执行 Effect
  • 后果: 如果 Effect 内部又触发了重新渲染,恭喜你,死循环达成。

⑨ 如何实现“深比较”依赖?

如果你非要根据对象内容来触发副作用,有三种主流方案:

方案 A:手动拆解(最推荐,简单粗暴)

不要把整个对象传进去,只传你需要的那几个属性(基本类型)。

JavaScript

useEffect(() => {
  // 业务逻辑
}, [userInfo.id, userInfo.name]); // ✅ 字符串/数字的比较是稳定的

方案 B:使用 useMemouseCallback

在父级或声明处就把对象锁死,保证引用不变。

JavaScript

const userInfo = useMemo(() => ({ id: 1, name: '张三' }), []); 

useEffect(() => { ... }, [userInfo]); // ✅ 引用地址现在稳定了

方案 C:硬核黑科技 useDeepCompareEffect

如果你面对的是极其复杂的嵌套对象(比如配置项),可以模仿社区库(如 ahooks)实现一个深比较 Hook:

JavaScript

import { useRef } from 'react';
import { isEqual } from 'lodash'; // 借用 lodash 的深比较

function useDeepCompareEffect(callback, dependencies) {
  const memoizedDeps = useRef([]);

  // 如果深比较发现不一样,才更新 ref
  if (!isEqual(memoizedDeps.current, dependencies)) {
    memoizedDeps.current = dependencies;
  }

  useEffect(callback, memoizedDeps.current);
}

总结

useEffect 已经不是你的业务逻辑中心,
而是“失控副作用的收容所”。

你应该:

- 把数据请求交给 React Query (非必要)

  • 把状态交给 state 管理库(Zustand / Jotai / Redux Toolkit)
  • 只在 effect 里写副作用(定时器、事件绑定、订阅等)
  • 用自定义 Hook 封装逻辑

写干净的组件,做干净react developers