什么是 Hooks?
简单来说,Hooks 是让函数组件拥有类组件功能的特殊函数。在 Hooks 出现之前,函数组件只能是无状态的,想要使用状态管理、生命周期等功能,就必须使用类组件。而有了 Hooks,我们可以在函数组件中轻松实现这些功能,让代码更加简洁、易懂。
想象一下,组件的生命周期就像一条线,从挂载、更新到卸载,而 Hooks 就像一个个钩子,能在这条线上的特定位置挂接我们需要执行的代码,这也是 “钩子” 这个名字的由来。
常用 Hooks 详解
1. useState:组件的状态管家
useState是最基础也最常用的 Hooks 之一,它的作用是为函数组件添加状态管理功能。
基本用法:
const [state, setState] = useState(initialValue);
这里的state是我们定义的状态变量,setState是用于更新状态的函数,initialValue是状态的初始值,决定state的类型。
特点:
- 不支持异步更新后的立即获取新值。如果需要基于前一个状态计算新状态,应该使用函数形式:
setState(prevState => prevState + 1);
-
每次调用
setState都会触发组件重新渲染。 -
可以在一个组件中多次使用
useState,管理不同的状态:
const [name, setName] = useState('');
const [age, setAge] = useState(18);
使用场景:管理组件中的各种状态,如表单输入值、开关状态、计数器等简单状态。
2. useEffect:处理副作用的多面手
useEffect被称为 “副作用函数”,能在组件的不同生命周期阶段执行我们需要的操作。
基本用法:
useEffect(() => {
// 函数体:要执行的操作
return () => {
// 清理函数:组件卸载时执行的操作
};
}, [依赖项]);
作用:在函数组件中模拟类组件的生命周期函数,让我们可以在组件的不同阶段执行代码。
执行时机:
-
组件每次加载(挂载)时会触发。
-
当
useEffect的第二个参数(依赖项数组)中的值发生变化时,会重新执行。 -
当依赖项为空数组
[]时,只会在组件初次渲染时执行一次。 -
第一个参数函数内部返回的新函数,会在组件被卸载时执行,用于清理资源。
常见用途:
-
数据获取:在组件挂载后从服务器获取数据
-
订阅事件:如监听窗口大小变化、滚动事件等
-
DOM 操作:对 DOM 元素进行修改
-
清理操作:如清除定时器、取消订阅等
示例:
useEffect(() => {
// 组件挂载时获取数据
fetchData();
// 组件卸载时取消订阅
return () => {
unsubscribe();
};
}, []); // 空数组表示只在初次渲染时执行
3. useLayoutEffect:同步执行的 DOM 操作专家
useLayoutEffect的用法和useEffect非常相似,但它们的执行时机有所不同。
特点:
-
useLayoutEffect中的函数会在所有 DOM 变更后同步执行,而且是在浏览器绘制之前。 -
而
useEffect中的函数是在浏览器绘制之后异步执行的。
使用场景:
import React, { useState, useEffect, useLayoutEffect } from 'react';
function LayoutEffectDemo() {
const [height, setHeight] = useState(100);
const divRef = React.createRef();
// 使用 useEffect(异步执行,在浏览器绘制后)
useEffect(() => {
console.log('useEffect: 测量高度前 -', divRef.current?.offsetHeight);
setHeight(divRef.current?.offsetHeight + 50);
console.log('useEffect: 设置新高度 -', height);
}, []);
// 使用 useLayoutEffect(同步执行,在浏览器绘制前)
useLayoutEffect(() => {
console.log('useLayoutEffect: 测量高度前 -', divRef.current?.offsetHeight);
setHeight(divRef.current?.offsetHeight + 50);
console.log('useLayoutEffect: 设置新高度 -', height);
}, []);
return (
<div ref={divRef} style={{ height, backgroundColor: 'lightblue' }}>
当前高度: {height}px
</div>
);
}
export default LayoutEffectDemo;
当我们需要在 DOM 更新后立即进行一些操作,并且这些操作会影响到页面的布局时,就应该使用useLayoutEffect。比如测量 DOM 元素的尺寸并根据尺寸进行布局调整,使用它可以避免页面出现闪烁。
注意:由于是同步执行,过度使用useLayoutEffect可能会影响性能,所以在大多数情况下,应该优先使用useEffect。
4. useReducer:复杂状态的管理者
当组件的状态逻辑比较复杂时,useState就显得有些力不从心了,这时useReducer就该登场了。
基本用法:
const [state, dispatch] = useReducer(reducer, initialState);
其中,reducer是一个处理状态更新的函数,initialState是初始状态,state是当前状态,dispatch是用于触发状态更新的函数。
reducer 函数格式:
const reducer = (state, action) => {
// 根据action.type处理不同的状态更新
switch (action.type) {
case 'TYPE1':
return newState1;
case 'TYPE2':
return newState2;
default:
return state;
}
};
重要规则:
-
在
reducer函数中不能直接修改原state,必须返回一个新的对象。 -
reducer函数应该是纯函数,即相同的输入始终返回相同的输出,不产生副作用。
使用技巧:可以结合immer库使用,简化状态更新的写法,不必手动复制原状态。
使用场景:
-
当状态逻辑复杂,包含多个子值时
-
当状态更新依赖于之前的状态时
-
当不同的操作需要修改同一状态时
示例
// 定义 reducer 函数
const counterReducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload }; // payload 是可选的携带数据
default:
throw new Error('未知 action');
}
};
// 在组件中使用
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
Reset
</button>
</div>
);
}
5. useRef:DOM 元素的抓手
useRef主要有两个用途:获取 DOM 结构和存储不需要触发重新渲染的值。
基本用法:
const refContainer = useRef(initialValue);
获取 DOM 结构:
通过将refContainer赋值给元素的ref属性,我们可以轻松获取 DOM 元素:
const inputRef = useRef(null);
// 在JSX中
<input ref={inputRef} />
// 之后就可以通过inputRef.current访问该DOM元素
inputRef.current.focus(); // 让输入框获得焦点
存储值:
useRef创建的对象有一个current属性,我们可以用它来存储任何值,而且修改这个值不会触发组件重新渲染:
const timerRef = useRef(null);
// 存储定时器ID
timerRef.current = setInterval(() => {
// 执行操作
}, 1000);
// 清除定时器
clearInterval(timerRef.current);
6. useContext:跨组件数据传递的桥梁
在 React 应用中,组件嵌套是很常见的情况,当我们需要在跨多层的组件之间传递数据时,useContext就派上用场了。
使用步骤:
- 创建 Context:
const MyContext = createContext(defaultValue);
-
提供 Context 值:
使用
MyContext.Provider组件包裹需要接收数据的组件树,并通过value属性提供数据:
<MyContext.Provider value={data}>
// 子组件树
</MyContext.Provider>
- 使用 Context 值:
在需要使用数据的组件中,通过useContext获取:
const data = useContext(MyContext);
优点:
-
可以跨越多层组件传递数据,避免了 “props drilling”(props 层层传递)的问题
-
当提供的
value发生变化时,所有使用该 Context 的组件都会重新渲染
使用场景:
-
主题设置(如深色 / 浅色模式)
-
用户登录状态
-
多语言切换等需要全局共享的数据
总结
| Hook | 核心功能 | 语法 | 应用场景 | 注意事项 |
|---|---|---|---|---|
useState | 为函数组件添加状态管理能力,支持状态更新和重新渲染。 | const [state, setState] = useState(initialValue); | 简单状态管理(如计数器、表单值、开关状态)。 | 1. 状态更新是异步的,多次调用可能合并。 2. 复杂状态(对象 / 数组)需返回新对象。 |
useEffect | 处理副作用(如数据获取、DOM 操作、订阅等),模拟类组件生命周期。 | useEffect(() => { /* 副作用代码 */ return () => { /* 清理函数 */ }; }, [dependencies]); | 1. 组件挂载 / 卸载时执行操作。 2. 依赖项变化时触发更新。 | 1. 依赖项必须包含所有外部变量。 2. 清理函数在组件卸载前执行。 |
useLayoutEffect | 与 useEffect 类似,但在 DOM 更新后、浏览器绘制前同步执行,用于避免视觉闪烁。 | 同 useEffect | 1. 需要立即读取 DOM 布局并同步更新。 2. 实现平滑动画过渡。 | 同步执行可能阻塞渲染,优先使用 useEffect。 |
useReducer | 复杂状态管理,将状态逻辑集中在 reducer 函数中,适合多子值或状态依赖场景。 | const [state, dispatch] = useReducer(reducer, initialState); | 1. 状态逻辑复杂(如购物车、表单校验)。 2. 下一个状态依赖于前一个状态。 | 1. reducer 必须是纯函数,返回新状态。 2. 可结合 immer 简化不可变操作。 |
useRef | 创建可变的 ref 对象,存储不需要触发渲染的值(如 DOM 元素、定时器 ID)。 | const refContainer = useRef(initialValue); | 1. 获取 DOM 元素引用。 2. 存储上一次的值(如 prevProps)。 | 修改 ref.current 不会触发组件重新渲染。 |
useContext | 跨组件层级共享状态,避免 props 层层传递。 | const value = useContext(MyContext); | 1. 全局状态(如主题、用户认证)。 2. 多层级组件间的数据传递。 | 频繁使用可能导致组件耦合,建议结合状态管理库使用。 |