掌握这 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 中,useLayoutEffect 是 useEffect 的孪生兄弟。
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,实现跨组件树数据共享 |