领导让我从vue转到react,怎么办尼,那就先看关键hooks,useRffect的使用吧🍭🍭🍭
掘金上关于 useEffect 的文章不少,
但真正把它 讲清楚、讲透、讲到你能写出可维护代码 的,其实不多😉😉😉。
今天哥们直接用几个你能马上复制运行的 Demo,
从最基础的依赖数组、清理副作用、到自定义 Hook,
再扩展到 TanStack Query 为什么几乎取代 useEffect。
一句话:
看完这篇,你对所有 "useEffect 什么时候写 / 写什么 / 不写会怎样" 都有清晰答案。
① useEffect 的本质:依赖数组才是核心
先来看最经典的例子:
scss
useEffect(() => {
console.log("Effect 执行,依赖 count =", count);
}, [count]);
只要你理解下面这句话,你就能掌握 useEffect:
依赖变 → 执行 effect → 执行 cleanup清理函数(如果有)
来看看 Demo:
javascript
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 故意的。
开发两次 → 线上一次
xml
//main.tsx
<StrictMode>
<APP>
</StrictMode>
② 自定义 Hook 其实就是"把 useEffect 封装起来"
你写过下面这种做"localStorage 持久化"的逻辑吗?
typescript
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];
}
页面使用:
arduino
const [name, setName] = useLocalStorage("demo-name", "sss");
本质是什么?
就是:
自定义 Hook = 包装 useState / useEffect,让你的页面更干净、逻辑复用。
你返回什么,外面就用什么。
完全等于"把 effect 写内部,外面只管拿结果"。
这也是为什么:
自定义 Hook 一般都带 use 开头,并且内部可能有多个 useEffect。
自定义hooks能不能不用use开头?
任何函数内部如果调用了 useState/useEffect,就必须以 use 开头,不然就是破坏 React 的 Hook 机制。其实我自己试了,不用use开头似乎也能正常运行(😕😕😕)
③ 为什么必须写 cleanup?(比如定时器)
下面这个 Demo 很多人面试必被问:
javascript
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 的神级用法
防抖逻辑:
scss
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;
}
使用:
scss
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 来请求数据了。
传统写法:
ini
useEffect(() => {
axios.get("/api/user").then(res => setUser(res.data));
}, []);
有:
- loading 状态
- error 状态
- 缓存策略
- 重试逻辑
- refetch 机制
写成地狱。
而 React Query:
php
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 已经不是你的业务逻辑中心,
而是"失控副作用的收容所"。
你应该:
- 把数据请求交给 React Query (非必要)
- 把状态交给 state 管理库(Zustand / Jotai / Redux Toolkit)
- 只在 effect 里写副作用(定时器、事件绑定、订阅等)
- 用自定义 Hook 封装逻辑
写干净的组件,做干净react developers