react hooks速通笔记

掌握这 8 大核心 Hooks,React 进阶才算真正入门

在 React 开发中,Hooks 的引入彻底改变了我们的状态管理与副作用处理方式。但在实际项目中,很多同学只是"会用",却常常因为对依赖项、闭包和组件封装性的理解不足,写出有性能隐患或破坏封装性的代码。

本文系统化地抽离了项目中最常用的 8 大核心 Hooks 场景,带你从"能跑就行"迈向"优雅优雅再优雅"。

一、 状态管理的基石:useState

useState 是最基础的 Hook,用于在函数组件中引入局部状态。但在面对连续的状态更新时,很多同学容易踩中"闭包陷阱"。

核心代码抽离

JavaScript

javascript 复制代码
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleAdd() {
    // 错误姿势:setCount(count + 1); 如果连续调用三次,最终只会加 1
    // 正确姿势:使用函数式更新,接收最新的 state
    setCount(prev => prev + 1);
  }

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={handleAdd}>自增</button>
    </div>
  );
}

二、 复杂状态的解耦利器:useReducer

当一个组件内存在多个状态相互关联,或者状态连续变更逻辑较复杂时,满屏的 useState 会让代码变成面条。此时,利用 useReducer 将状态更新逻辑抽离到组件外部,是单向数据流的最佳实践。

核心代码抽离

JavaScript

javascript 复制代码
import { useReducer } from "react";

// 1. 将状态更新逻辑抽离到组件外部,保持纯净
function countReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return state + 1;
    case 'REDUCE':
      return state - 1;
    default:
      return state; 
  }
}

export default function CounterApp() {
  // 2. 声明式初始化
  const [state, dispatch] = useReducer(countReducer, 0);

  return (
    <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
      <button onClick={() => dispatch({ type: "REDUCE" })}>-</button>
      <span>{state}</span>
      <button onClick={() => dispatch({ type: "ADD" })}>+</button>
    </div>
  );
}

💡 掘金小贴士reducer 函数必须是一个纯函数。不要在里面进行网络请求、修改外部变量或产生任何副作用。

三、 页面初次加载的副作用防线:useEffect 的清理艺术

useEffect 用于处理网络请求、订阅、定时器等副作用。处理不好极易引发内存泄漏死循环请求

核心代码抽离

JavaScript

javascript 复制代码
import { useState, useEffect } from "react";

export default function UserProfile() {
  const [name, setName] = useState("加载中...");

  useEffect(() => {
    // 1. 模拟网络请求
    const timer = setTimeout(() => {
      setName("张三"); 
    }, 1500);

    // 2. 核心优雅姿势:返回清理函数,组件卸载时清除定时器,严防内存泄漏
    return () => clearTimeout(timer);
    
  }, []); // 3. 👈 空数组保证该副作用"只在组件初次挂载时"执行一次

  return (
    <div style={{ padding: '20px' }}>
      <h2>用户信息</h2>
      <p>用户名:{name}</p>
    </div>
  );
}

四、 跨渲染周期的"记忆胶囊":useRef

大部分人只知道用 useRef 拿 DOM 节点,但它还有一个神级特性:修改 useRef.current 的值绝对不会触发组件重新渲染。利用这个特性,它可以充当一个在组件整个生命周期中都不会丢失的静态存储空间。

核心代码抽离

JavaScript

javascript 复制代码
import { useState, useRef, useEffect } from "react";

export default function TimerApp() {
  const [count, setCount] = useState(0);
  
  // 1. 用 useRef 存储定时器 ID,修改它不会触发页面刷新
  const timerRef = useRef(null);

  const startTimer = () => {
    if (timerRef.current !== null) return; 
    
    timerRef.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    // 2. 直接从 ref 中拿到最新的定时器 ID 销毁它
    if (timerRef.current !== null) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

  useEffect(() => {
    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
    };
  }, []);

  return (
    <div>
      <p>计时器:{count} 秒</p>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer} style={{ marginLeft: "10px" }}>暂停</button>
    </div>
  );
}

五、 穿透挂与权限控制:forwardRef + useImperativeHandle

在 React 中,父组件直接拿子组件的 DOM 节点会破坏组件的封装性。但有时父组件确实需要调用子组件的特定方法(如重置表单或聚焦)。useImperativeHandle 配合 forwardRef 就是这把精细控制暴露权限的钥匙。

核心代码抽离

JavaScript

javascript 复制代码
import { useRef, forwardRef, useImperativeHandle } from 'react';

// 1. 使用 forwardRef 让子组件有能力接收 ref 指针
const Child = forwardRef(function (props, ref) {
  
  // 2. 关键权限控制:显式自定义映射,只暴露父组件需要的方法
  useImperativeHandle(ref, () => ({
    focusAndAlert: () => {
      alert("这是子组件内部的方法,被父组件成功穿透调用!");
    }
  }));

  return <div style={{ border: '1px dashed #aaa', padding: '10px' }}>我是子组件</div>;
});

export default function ParentApp() {
  const childRef = useRef();

  function handleClick() {
    // 3. 只能调用到暴露的方法,其余内部状态被完美隐藏
    childRef.current?.focusAndAlert();
  }

  return (
    <div style={{ padding: '20px' }}>
      <Child ref={childRef} />
      <button onClick={handleClick} style={{ marginTop: '10px' }}>穿透调用子组件方法</button>
    </div>
  );
}

六、 阻断子组件误触发渲染:useCallback + React.memo

在 React 中,父组件只要重新渲染,其内部声明的函数每次都会生成一个新的内存地址 。如果这个函数作为 props 传给子组件,即使子组件加了 React.memo,也会因为属性地址改变而跟着白白渲染。

核心代码抽离

JavaScript

javascript 复制代码
import { useState, useCallback, memo } from "react";

// 1. 子组件用 memo 包裹:只有 props 真正改变时才会重新渲染
const Child = memo(function ({ onAdd }) {
  console.log("子组件渲染了");
  return (
    <div style={{ margin: '10px 0', padding: '10px', background: '#f5f5f5' }}>
      <button onClick={onAdd}>从子组件触发父亲的方法</button>
    </div>
  );
});

export default function RenderApp() {
  const [count, setCount] = useState(0);

  // 2. 正确姿势:使用 useCallback 锁死函数的内存地址
  // 3. 内部使用函数式更新,依赖项写 []
  const memoizedAdd = useCallback(() => {
    setCount(prevCount => prevCount + 1); 
  }, []); 

  return (
    <div>
      <p>计数器:{count}</p>
      <button onClick={memoizedAdd}>增加</button>
      {/* 4. 此时 memoizedAdd 地址固定,Child 成功被 memo 拦截 */}
      <Child onAdd={memoizedAdd} />
    </div>
  );
}

七、 疯狂打字也不卡顿的秘密:useMemo 缓存复杂计算

有时候我们需要在前端做一些大数据量的处理、过滤或循环计算。如果不做缓存,用户在页面的其他无关输入框里疯狂打字时,组件频繁刷新,这段重计算也会跟着反复执行,导致整页卡死。

核心代码抽离

JavaScript

ini 复制代码
import { useState, useMemo } from "react";

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

  // 1. 正确姿势:用 useMemo 把耗时计算包裹起来,并准确填写依赖项 [count]
  const expensiveResult = useMemo(() => {
    console.log("正在进行极其耗时的计算...");
    let result = 0;
    for (let i = 0; i < 100000000; i++) {
      result += count;
    }
    return result;
  }, [count]); // 👈 只有 count 改变时,才会重新触发百万次循环

  return (
    <div style={{ padding: '20px' }}>
      <input 
        placeholder="在这里输入文字,体验丝滑度" 
        value={text} 
        onChange={e => setText(e.target.value)} 
      />
      <p>当前 count: {count},计算结果:{expensiveResult}</p>
      <button onClick={() => setCount(prev => prev + 1)}>修改 count 触发重计算</button>
    </div>
  );
}

八、 斩断 Props 逐层传递的痛苦:useContext 跨层级共享

当组件树嵌套得太深时,如果用 Props 一层层传下去(Props Drilling),中间的组件即使不用这些数据也必须当"传话筒"。useContext 允许你创建一个上下文,内部的任何一个子组件都可以直接"隔空取物"。

核心代码抽离

JavaScript

javascript 复制代码
import React, { createContext, useContext } from 'react';

// 1. 创建一个 Context
const LevelContext = createContext(0);

// 2. 消费级子组件:通过 useContext 直接读取祖先提供的值
function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0: return <h1>{children}</h1>;
    case 1: return <h2>{children}</h2>;
    case 2: return <h3>{children}</h3>;
    default: return <p>{children}</p>;
  }
}

// 3. 生产级组件:包裹 Provider,每嵌套一层,level 自动 +1
function Section({ children }) {
  const currentLevel = useContext(LevelContext);
  const nextLevel = currentLevel + 1;

  return (
    <LevelContext.Provider value={nextLevel}>
      <section style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}>
        {children}
      </section>
    </LevelContext.Provider>
  );
}

export default function ContextApp() {
  return (
    <div>
      <Heading>网站主标题 (h1)</Heading>
      <Section>
        <Heading>第一部分标题 (h2)</Heading>
        <Section>
          <Heading>1.1 子章节标题 (h3)</Heading>
        </Section>
      </Section>
    </div>
  );
}

进阶彩蛋:useLayoutEffect 是什么?

在经典的 8 大 Hooks 中,useLayoutEffectuseEffect 的孪生兄弟。

  • useEffect :在浏览器把东西画到屏幕上之后异步执行(可能会看到内容闪烁)。
  • useLayoutEffect :在 DOM 结构更新完、但浏览器还没画到屏幕上之前同步执行。通常用来在元素显示前精确测量或修改 DOM 的大小、位置,从而彻底防范视觉闪烁。

总结秘籍

Hook 核心场景 优雅心法
useState 基础局部状态 连续更新请用函数式回调 prev => prev + 1
useReducer 复杂/关联状态 将状态逻辑移出组件,保持 Reducer 纯净
useEffect 异步副作用 别忘了解绑和清理函数,严防内存泄漏
useRef 静态存储/DOM 引用 修改 .current 不引发重渲染,完美的记忆胶囊
forwardRef + useImperativeHandle 精准控制子组件暴露 保护封装性,拒绝直接暴露整个 DOM 实例
useCallback 缓存函数引用 配合 React.memo 阻断子组件无意义重渲染
useMemo 缓存计算结果 把计算压力留给内存,把丝滑打字留给用户
useContext 跨组件层级传递 斩断 Props Drilling,实现跨组件树数据共享
相关推荐
Csvn1 小时前
🚨 组件卸载后还在 setState?一个被你忽视的内存泄漏和报错根源
前端
乘风gg1 小时前
AI GenUI 真正落地时,前端到底要做什么?
前端·ai编程·cursor
恋猫de小郭2 小时前
苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力
android·前端·flutter
IT_陈寒2 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
kyriewen14 小时前
别再 console.log 了:5 个 Chrome DevTools 调试技巧,用过就回不去了
前端·javascript·面试
IT_陈寒16 小时前
Python搞不定字符串编码?这破玩意坑我两小时!
前端·人工智能·后端
DigitalOcean17 小时前
Laravel 开发者已在 DigitalOcean 上开通超过 10 万台服务器
前端·laravel
星始流年17 小时前
从 Tool 到 Skill——基于 LangChain 的服务端Skill实现
前端·langchain·agent
李惟17 小时前
开源本地通信库,纯客户端 RPC,像聊天一样通信
前端