一文讲清 16 个 React 常用钩子的机制和使用场景
时间:2026-4-24 14:26 作者:独元殇 分类: 前端技术
[toc]
今天我整理一下笔记,把我以前记录的下面几个 react 钩子的概念和使用方式、场景简单的过一下。
是我以前在 dbuke 这个在线教程网站上学习时候做的笔记。现在这个网站貌似打不开了。
一共 6 类,16 个。这应该是涵盖 react 使用里的所有的常用钩子了(没包括一些 react 19 的),涵盖 React 日常使用!本文是手打的,不是 AI 文。
/ 01 / State Management(状态管理)
useState - 定义组件状态
useReducer - 复杂状态管理
useSyncExternalStore - 订阅外部数据源
/ 02 / Effect Hooks(副作用)
useEffect - 处理副作用
useLayoutEffect - 同步执行副作用
useInsertionEffect - 注入样式专用
/ 03 / Ref Hooks(引用)
useRef - 创建可变引用
useImperativeHandle - 暴露子组件方法
/ 04 / Performance Hooks(性能优化)
useMemo - 缓存计算结果
useCallback - 缓存函数引用
Context Hooks(上下文)
useContext - 读取上下文值
/ 05 / Transition Hooks(过渡)
useTransition - 标记非紧急更新
useDeferredValue - 延迟更新状态值
/ 06 / Random Hooks(杂项)
useDebugValue - 自定义 Hook 调试
useId - 生成唯一 ID
一共 6 类,16 个。
不深入细讲,只是简单每个都说说。
State Management(状态管理)
先说我们最常用的钩子。useState 是用到最频繁的一个。
因为 React 诞生的重要目的就是管理状态,统一管理应用状态!也就是在状态发生变化时,自动重新渲染组件。
useState - 定义组件状态
const [value, setValue] = useState(''); // 设置一个状态初始值
const handleChange = (e) => {
setValue(e.target.value); // 事件一激活,就改值
};
<input
type="text"
value={value}
onChange={handleChange}
/>
这个值存在第一个变量 value 里,第二个变量 setValue 是一个修改状态的函数。上面这个例子,我们在 input 里面输入什么,它就马上等于什么了。
useReducer - 复杂状态管理
useReducer 是另一个状态钩子,可以管理比 useState 更复杂的状态。这个用的少,但更强大。
const reducer = (state, action) => {
switch (action) {
case 'increment':
return state + 1;
default:
return state;
}
};
const [count, dispatch] = useReducer(reducer, 0);
<button onClick={() => dispatch('increment')}>Increment</button>
这个 dispatch 新函数,里面有个关键字 action,可以接收我们传入给 dispatch 的参数,然后做出更灵活的变化。
尤其是游戏界面这种,界面复杂、交互复杂的,会更好用。
useSyncExternalStore - 订阅外部数据源
然后是 useSyncExternalStore,这个钩子比较冷门。它可以把非 React 的状态,接入到 React 组件中。
Effect Hooks(副作用)
useEffect - 处理副作用
副作用钩子 useEffect,指的是组件渲染中与渲染结果无关联、但会影响环境状态的一些操作。
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `你点击了 ${count} 次`;
}, [count]);
<button onClick={() => setCount(count + 1)}>
Click me
</button>
什么意思呢?看上面这段代码,useEffect 的第二个参数里传入了 count,那么只要这个 count 变化,就会执行里面的函数。
注意,useEffect 不应该用于”点击按钮时执行”、”获取数据时执行”这种场景。
其实并不需要经常使用这个钩子。一般我们用 useEffect 是在需要让 React 与浏览器 API 同步的时候,比如用 React 控制视频(或音频)的播放、暂停:
const ref = useRef(null); // 创建"引用",抓住视频 DOM 元素
useEffect(() => {
if (isPlaying) {
ref.current.play(); // 播放
} else {
ref.current.pause(); // 暂停
}
}, [isPlaying]);
<video ref={ref} src={src} loop playsInline />
当我们把 isPlaying 设置成 true 时,视频就开始播放了。这种,才是 useEffect 真正合适的场景!
useLayoutEffect - 同步执行副作用
其实还有个差不多的钩子,某些场景下更好用,叫 useLayoutEffect。它是在界面渲染之前就执行的。使用例子,在渲染前就获取某元素的高度:
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect(); // 获取提示框高度
setTooltipHeight(height); // 设置高度
}, []);
厉害吧!用它,浏览器还没开始渲染这个提示框,我们就获取到了这个提示框的高度,然后保存在状态里。
非常好用,可以防止页面闪烁跳跃,懂吧?
useInsertionEffect - 注入样式专用
既然比 useEffect 早,对吧?其实还有更早的。
还有一个很小众的钩子 useInsertionEffect,它专门为 CSS 和 JS 的库开发。这个运行得更早,比 useLayoutEffect 还早!专门用来确保所有的 CSS 样式都能应用到正确的组件元素上。
Ref Hooks(引用)
useRef - 创建可变引用
说说 useRef 钩子吧。
它类似于 useState,也能存状态,不过它不会触发组件渲染,所以更简单了!useRef 叫引用。
引用是可变的,而状态是不可变的。因此能使用等号运算符直接修改,比如 ref.current = 'something'。下面是一个计时器使用例子:
// 一个计时器使用案例
const [timer, setTimer] = useState(0);
const intervalRef = useRef(); // useRef 用来保存 setInterval 的 ID
const startTimer = () => {
intervalRef.current = setInterval(() => { // 存进去了
setTimer((prevTimer) => prevTimer + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalRef.current); // 停止时就能用到
};
<p>Timer: {timer} seconds</p>
<button onClick={startTimer}>开始计时</button>
<button onClick={stopTimer}>停止计时</button>
你可能会问,为什么不直接用变量呢?因为 React 组件一更新,变量就没了…… 所以使用 React 内部的值,肯定是有用的!
useImperativeHandle - 暴露子组件方法
有个略冷门的钩子:useImperativeHandle。
转发一个引用的时候会用到。比如:
function ParentComponent() {
const inputRef = useRef();
const handleFocus = () => {
inputRef.current.focus();
};
// ...
return (
<>
<CustomInput ref={inputRef} />
<button onClick={handleFocus}>激活 input</button>
</>
);
}
// 上面的代码肯定无法运行
// 因此我们使用 forwardRef + useImperativeHandle,把方法暴露出去
// 把子组件传递给父组件,然后就可以用了
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
});
因为直接给自定义组件(如 CustomInput)传 ref,默认是拿不到 DOM 元素的。
Performance Hooks(性能优化)
useMemo - 缓存计算结果
再说说 useMemo 这个钩子。
它用一种叫”记忆化”的方法来缓存之前的结果,以此提升应用性能。它只会在依赖项发生变化时重新计算缓存的值,非常适合执行高消耗的计算,举个例子:
function SumComponent({ numbers }) {
const sum = useMemo(() => { // 把求和的结果缓存起来
return numbers.reduce((total, n) => total + n, 0);
}, [numbers]);
return <h1>合计: {sum}</h1>;
}
把求和的结果存起来,组件再渲染时就不会重复计算了。(只有 numbers 变化时,才会重新缓存)
useCallback - 缓存函数引用
和 useMemo 差不多,有个叫 useCallback 的钩子。
这个能缓存函数!!
比如这个例子:
function Counter() {
const [count, setCount] = useState(0);
// 单击后会执行
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<>
<div>{count}</div>
<Button onClick={increment} />
</>
);
}
// button 组件
function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}
因为有个问题:每次重新渲染,里面的函数都会被重新创建。那我们只要 useCallback 一下,下次再渲染,就不用重复创建 increment 函数了。
useContext - 读取上下文值
再说说 useContext 这个钩子吧。它能读取上下文(可理解为 React 版全局变量)的值:
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
const theme = useContext(ThemeContext);
// ...
}
createContext 创建了一个值(React 版全局变量),然后这个值通过 Provider 方法给里面填充(value 放什么,就是什么),之后我们在另一个组件里(比如 Form 里)使用 useContext 就可以访问到了。
这个常用于主题逻辑。
Transition Hooks(过渡)
useTransition - 标记非紧急更新
再说说 useTransition 钩子。
这是一个过渡钩子,我们可以用它来指定某些状态的更新。这个在处理涉及大量计算的状态更新时用处很大!因为状态更新如果被立即执行,可能会导致不良的用户体验。什么意思呢?比如这个例子:
const [filter, setFilter] = useState('');
const [inputValue, setInputValue] = useState('');
const [isPending, startTransition] = useTransition();
const filteredItems = items.filter(item => item.includes(filter));
<input
value={inputValue}
onChange={(event) => {
setInputValue(event.target.value);
startTransition(() => {
setFilter(event.target.value);
});
}}
placeholder="请填充内容...."
/>
{isPending ? (
<p>加载中...</p>
) : (
filteredItems.map(item => <div key={item}>{item}</div>)
)}
根据用户输入,来过滤一个列表。
但是在输入框中输入时,每次输入都重新渲染,会导致 UI 卡顿。
然后我们就用 useTransition 这个钩子里的 startTransition 包裹一下,这段逻辑就被标记为”非紧急”了。
另一个值 isPending 是一个布尔值,告诉你当前是否还有状态在等待中,这样我们就可以在状态更新完成之前,给用户显示一个”加载中……”。
useDeferredValue - 延迟更新状态值
再说说 useDeferredValue 这个钩子。
它和 useTransition 很相似,不过它可以在最合适的时机更新状态。
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
一个完整的例子如下:
function FilteredList({ items }) {
const [inputValue, setInputValue] = useState('');
const deferredFilter = useDeferredValue(inputValue);
const filteredItems = items.filter(item =>
item.toLowerCase().includes(deferredFilter.toLowerCase())
);
return (
<>
<input
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Type to filter..."
/>
{filteredItems.map(item => <div key={item}>{item}</div>)}
</>
);
}
useDeferredValue 很适合像过滤列表这样的场景。无需你手动设置过渡,也不需要任何异步状态。
Random Hooks(杂项)
useId - 生成唯一 ID
说一下 useId 钩子,这是一个随机钩子。
调用它时,会创建一个唯一的 ID。
它一般用于在表单输入框之间共享的 id。什么意思呢?看看这个例子:
function EmailInput({ name }) {
const id = useId();
return (
<>
<label htmlFor={id}>{name}</label>
<input id={id} type="email" />
</>
);
}
function Form() {
return (
<form>
<EmailInput name="Email" />
<EmailInput name="Confirm Email" />
</form>
);
}
看下面的 Form 里,两次用了 EmailInput 组件,为了防止里面的 input 冲突,于是我们就用 useId 钩子。