React Hooks 学习(从入门到实战心智)

3 阅读4分钟

目标:用最少的心智负担,把 Hooks 用“对”、用“稳”、用“快”。
适合:刚学完 React、正在写项目(如路由 + Redux/RTK + axios + styled-components)的同学。


1. Hooks 是什么?解决了什么问题?

Hooks 是 React 在函数组件里提供的一套能力,让你不用 class 也能:

  • 管理状态(state)
  • 处理副作用(effect)
  • 复用逻辑(custom hook)
  • 做性能优化(memo / useMemo / useCallback)
  • 读写全局状态(Context / Redux hooks)

一句话:Hooks 把“状态 + 副作用 + 复用”这三件事变得更自然、更可组合。


2. Hooks 的两条铁律

React 官方规则(不遵守会出大问题):

  1. 只能在函数组件或自定义 Hook 的顶层调用 Hook

    • 不能在 if / for / while / switch / try-catch 里调用
  2. 不要在普通函数里调用 Hook

    • 只能在 Component() 或 useXxx() 里

原因:React 需要依赖“调用顺序”来正确匹配每一个 Hook 的状态。


3. useState:最常用的状态管理

3.1 基本用法

const [count, setCount] = useState(0);

  • count 是当前状态
  • setCount 用来更新状态,更新会触发重新渲染

3.2 更新依赖旧值:用函数式更新更稳

setCount(prev => prev + 1);

适合“连续点击 / 多次更新”场景,避免闭包拿到旧值。


4. useEffect:副作用的核心

4.1 什么是“副作用”?

凡是**不是“根据 props/state 计算 UI”**的行为,都属于副作用,比如:

  • 请求接口(axios/fetch)
  • 订阅事件(addEventListener)
  • 操作 DOM
  • 定时器
  • localStorage

4.2 useEffect 的执行时机(超重要)

useEffect(() => {

// 副作用逻辑

return () => {

// 清理逻辑

};

}, [deps]);

  • 第一次渲染完成后执行一次
  • 依赖变化时:先执行上一次 cleanup,再执行新的 effect
  • 组件卸载时:执行 cleanup

4.3 依赖数组的三种写法(必会)

  • 不写依赖:每次渲染后都执行(很少用,容易出坑)
  • []:只在挂载时执行一次(常见:首次请求、订阅监听)
  • [a, b]:a/b 变化时重新执行(常见:随条件重新请求)

4.4 清理函数:防止“内存泄漏”

例:给 window 注册点击监听,组件卸载时必须移除。

useEffect(() => {

const onClick = () => setOpen(false);

window.addEventListener("click", onClick, true);

return () => {

window.removeEventListener("click", onClick, true);

};

}, []);


5. useRef:拿 DOM / 存“不会触发渲染”的值

5.1 获取 DOM

const inputRef = useRef(null);

< input ref={inputRef} />

然后:

inputRef.current?.focus();

5.2 保存“跨渲染持久”的变量(不触发更新)

比如计时器 id:

const timerRef = useRef(null);

timerRef.current = setTimeout(...)


6. useMemo:缓存“计算结果”

适用:某个计算很耗时,或者你需要“稳定的引用”避免子组件重渲染。

const total = useMemo(() => {

return expensiveCalc(list);

}, [list]);

注意:useMemo 是优化工具,不是必用工具。先正确,再优化。


7. useCallback:缓存“函数引用”

适用:把函数传给子组件,配合 memo 减少不必要的刷新。

const onTabClick = useCallback((index) => {

setCurrent(index);

}, []);

如果你不写 useCallback,父组件每次渲染都会创建一个新函数引用,子组件可能会以为 props 变了而重渲染。


8. memo:缓存“组件渲染结果”(避免重复渲染)

export default memo(function RoomItem(props) {

...

});

什么时候有用?

  • 子组件很复杂,渲染成本高
  • 父组件经常重渲染,但子组件 props 很少变

什么时候没必要?

  • 组件很简单
  • props 每次都在变(例如传新对象、新函数)

9. useContext:跨层级共享数据

适用:主题、语言、用户信息等。

const ThemeContext = createContext(null);

<ThemeContext.Provider value={theme}>

</ThemeContext.Provider>

子组件:

const theme = useContext(ThemeContext);

如果你的项目已经用了 Redux/RTK,一般:

  • 业务数据用 Redux
  • 主题/配置用 Context 或 styled-components 的 ThemeProvider

10. 自定义 Hook:把“可复用逻辑”抽出来

10.1 为什么要自定义 Hook?

当你发现多个组件里都有:

  • 相同的请求逻辑
  • 相同的滚动监听
  • 相同的表单逻辑
  • 相同的状态组合

就可以抽成 useXxx,让逻辑可复用、可测试、可维护。

10.2 示例:封装一个“点击外部关闭”的 Hook

function useClickOutside(handler) {

useEffect(() => {

const onClick = (e) => handler(e);

window.addEventListener("click", onClick, true);

return () => window.removeEventListener("click", onClick, true);

}, [handler]);

}

用法:

useClickOutside(() => setOpen(false));


11. 常见坑点合集

11.1 “依赖数组”写错导致死循环

在 effect 里 setState,又把那个 state 放进依赖里,可能循环触发。

思路:确认依赖是谁、谁变化需要重新执行。

11.2 闭包旧值问题

useEffect(() => {

setInterval(() => console.log(count), 1000);

}, []);

这里永远打印初始 count。解决:

  • 用依赖更新 effect
  • 或用 ref 保存最新值

11.3 不要把 Hook 当成语法糖乱用

  • useMemo/useCallback 不是写得越多越好
  • 优化要有目标:减少明显的重复渲染或重计算