11React Hooks核心知识点:从使用到原理的全面解析
引言:Hooks解决了什么问题?
在React 16.8之前,函数组件无法拥有状态,也不能处理副作用(如数据请求、事件监听),复杂逻辑的复用只能通过高阶组件(HOC)或Render Props实现,导致代码嵌套过深("嵌套地狱")。
Hooks的出现彻底改变了这一现状:它允许函数组件拥有状态和副作用,同时提供了更简洁的逻辑复用方式 。如今,Hooks已成为React开发的核心,理解Hooks是掌握现代React的关键。
一、常用Hooks的使用场景与注意事项
React提供了多个内置Hooks,每个Hooks都有明确的设计目的,掌握它们的使用场景是基础。
1.1 useState
:函数组件的状态管理
作用:为函数组件添加状态(state),并提供更新状态的方法。
基本用法:
const [count, setCount] = useState(0);
// count:当前状态值
// setCount:更新状态的函数(参数为新状态或计算新状态的函数)
使用场景:管理组件内部的简单状态(如表单输入、开关状态、计数器等)。
注意事项:
- 状态更新是异步的:
setCount
不会立即改变count
,而是触发组件重新渲染,新状态在下次渲染中生效。jsxconst handleClick = () => { setCount(count + 1); console.log(count); // 输出旧值(异步更新未生效) };
- 函数式更新:当新状态依赖旧状态时,用函数形式确保获取最新状态:jsx
// 正确:依赖旧状态时用函数式更新 setCount(prevCount => prevCount + 1);
- 初始值只执行一次:
useState(initialValue)
的initialValue
仅在组件首次渲染时计算,后续渲染忽略。若初始值计算昂贵,用函数形式延迟计算:jsx// 初始值计算昂贵时,用函数形式(只执行一次) const [data, setData] = useState(() => computeExpensiveData());
1.2 useEffect
:处理副作用
作用:在函数组件中处理副作用(如数据请求、事件监听、DOM操作等),替代类组件的生命周期方法。
基本用法:
useEffect(() => {
// 副作用逻辑(如请求数据、添加事件监听)
const timer = setInterval(() => console.log('tick'), 1000);
// 清理函数(可选):在组件卸载或依赖变化前执行
return () => {
clearInterval(timer); // 清除定时器,防止内存泄漏
};
}, [deps]); // 依赖数组:当依赖变化时,重新执行副作用
使用场景:处理需要在渲染后执行的逻辑(如数据请求)、需要清理的资源(如定时器、事件监听)。
注意事项:
- 依赖数组控制执行时机:
- 空数组
[]
:仅在组件挂载后执行一次(类似componentDidMount
); - 无依赖数组:每次渲染后都执行;
- 含依赖
[a, b]
:首次渲染+a
或b
变化时执行。
- 空数组
- 清理函数的作用:防止内存泄漏(如组件卸载后定时器仍在运行、事件监听未移除)。
- 不要遗漏依赖:若副作用中使用了组件内的变量/函数,必须加入依赖数组,否则可能捕获旧值(详见下文"闭包陷阱")。
1.3 useRef
:保存跨渲染周期的值
作用:创建一个"容器",用于存储跨渲染周期不变的引用值(如DOM元素、定时器ID、不需要触发重渲染的状态)。
基本用法:
// 1. 引用DOM元素
const inputRef = useRef(null);
// 渲染后:inputRef.current指向<input>元素
return <input ref={inputRef}/>;
// 2. 存储跨渲染的值(不触发重渲染)
const timerRef = useRef(null);
const startTimer = () => {
timerRef.current = setInterval(() => {
}, 1000); // 存储定时器ID
};
使用场景:
- 获取DOM元素(如操作输入框焦点:
inputRef.current.focus()
); - 存储不需要触发重渲染的数据(如定时器ID、上一次的状态值)。
注意事项:
ref.current
的变化不会触发组件重渲染(与useState
不同);- 不要在渲染阶段修改
ref.current
(可能导致不可预测的行为),应在事件处理或useEffect
中修改。
1.4 useContext
:简化上下文访问
作用:在函数组件中直接访问React Context的值,避免通过Consumer
或层层传递props
。
基本用法:
// 1. 创建Context
const ThemeContext = React.createContext('light');
// 2. 提供Context值(祖先组件)
const App = () => (
<ThemeContext.Provider value="dark">
<Child/>
</ThemeContext.Provider>
);
// 3. 消费Context(子组件)
const Child = () => {
const theme = useContext(ThemeContext); // 直接获取Context值
return <div>当前主题:{theme}</div>;
};
使用场景:跨组件共享数据(如主题、用户信息、权限等),替代"props透传"。
注意事项:
- 当
Provider
的value
变化时,所有使用useContext
的组件都会重渲染(即使父组件用了React.memo
); - 避免过度使用Context(可能导致不必要的重渲染),局部状态优先用
useState
。
1.5 useReducer
:复杂状态逻辑的管理
作用:通过" reducer函数"管理复杂状态(多个相关状态、复杂更新逻辑),类似Redux的简化版。
基本用法:
// 1. 定义reducer(纯函数:接收state和action,返回新state)
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, {id: Date.now(), text: action.text, done: false}];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? {...todo, done: !todo.done} : todo
);
default:
return state;
}
};
// 2. 使用useReducer
const [todos, dispatch] = useReducer(todoReducer, []);
// todos:当前状态
// dispatch:发送action的函数(用于触发状态更新)
// 3. 触发状态更新
const addTodo = (text) => {
dispatch({type: 'ADD_TODO', text}); // 发送action
};
使用场景:
- 状态逻辑复杂(如多个子值组成的状态,如
{ users: [], loading: false, error: null }
); - 状态更新依赖前一个状态(避免
useState
的函数式更新嵌套); - 需要通过Context共享状态更新逻辑(比传递多个
setState
函数更简洁)。
注意事项:
reducer
必须是纯函数(不修改入参state
,无副作用);dispatch
函数的引用在组件生命周期内保持不变(可安全地加入useEffect
依赖)。
二、useEffect
深度解析:副作用与生命周期
useEffect
是最常用也最容易出错的Hooks,理解其执行机制是掌握Hooks的关键。
2.1 依赖项:控制副作用的执行时机
useEffect
的第二个参数(依赖数组)决定了副作用何时执行:
无依赖数组:每次组件渲染后都执行副作用,清理函数在每次执行前触发(首次渲染后只执行副作用,无清理)。
jsxuseEffect(() => { console.log('每次渲染后执行'); return () => console.log('下次渲染前清理'); });
空依赖数组
[]
:仅在组件挂载后执行一次副作用,清理函数在组件卸载时执行(模拟componentDidMount
和componentWillUnmount
)。jsxuseEffect(() => { console.log('组件挂载后执行'); return () => console.log('组件卸载时清理'); }, []);
含依赖的数组
[a, b]
:首次渲染后执行,且当a
或b
的值发生变化时重新执行(浅比较),清理函数在依赖变化前或组件卸载时执行(模拟componentDidUpdate
)。jsxuseEffect(() => { console.log(`userId变化:${userId}`); return () => console.log(`userId即将更新:${userId}`); }, [userId]); // 仅userId变化时执行
2.2 清理函数:防止内存泄漏
清理函数是useEffect
返回的函数,作用是在副作用失效前清理资源,避免内存泄漏。常见场景:
事件监听:移除事件监听,防止组件卸载后仍触发回调。
jsxuseEffect(() => { const handleScroll = () => console.log('滚动了'); window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); // 清理 }; }, []);
定时器/间隔器:清除定时器,避免组件卸载后仍执行。
jsxuseEffect(() => { const timer = setInterval(() => console.log('tick'), 1000); return () => clearInterval(timer); // 清理 }, []);
订阅/请求取消:取消API请求(如Axios的
CancelToken
),避免组件卸载后收到过时响应。
2.3 模拟类组件生命周期
useEffect
可通过依赖数组模拟类组件的所有生命周期:
类组件生命周期 | useEffect 实现方式 |
---|---|
componentDidMount (挂载后) | useEffect(() => { ... }, []) |
componentDidUpdate (更新后) | useEffect(() => { ... }, [deps]) (依赖变化时) |
componentWillUnmount (卸载前) | useEffect(() => { return () => { ... } }, []) (清理函数) |
示例:模拟完整生命周期
const LifecycleDemo = ({userId}) => {
// 挂载后执行(componentDidMount)
useEffect(() => {
console.log('组件挂载完成');
}, []);
// userId变化时执行(componentDidUpdate)
useEffect(() => {
console.log(`userId更新为:${userId}`);
}, [userId]);
// 卸载前清理(componentWillUnmount)
useEffect(() => {
return () => {
console.log('组件即将卸载,清理资源');
};
}, []);
return <div>userId: {userId}</div>;
};
2.4 闭包陷阱:useEffect
中的旧值问题
useEffect
的副作用函数会"捕获"当前渲染周期的状态和变量,若依赖数组设置不当,可能导致访问旧值:
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用捕获了初始的count(0)
const timer = setInterval(() => {
console.log('当前count:', count); // 始终输出0(闭包陷阱)
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖:副作用只执行一次,捕获初始count
return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
};
解决方法:将依赖加入依赖数组,让副作用在依赖变化时重新执行,捕获新值:
useEffect(() => {
const timer = setInterval(() => {
console.log('当前count:', count); // 正确输出最新count
}, 1000);
return () => clearInterval(timer);
}, [count]); // 依赖count:count变化时重新执行副作用
三、自定义Hooks:逻辑复用的最佳实践
自定义Hooks是抽取和复用组件逻辑的利器,它允许你将组件中重复的逻辑(如数据请求、表单处理、订阅)封装成可复用的函数。
3.1 设计原则:什么是合格的自定义Hook?
- 命名必须以
use
开头:这是React的约定,确保React能识别它是Hooks(从而检查Hook调用规则)。 - 内部可调用其他Hooks:如
useState
、useEffect
等,但必须遵循Hook调用规则(不能在条件语句中调用)。 - 目的是复用逻辑,而非复用UI:自定义Hooks返回数据或函数,不返回JSX(UI复用用组件)。
- 独立作用域:每个组件使用自定义Hook时,内部的状态和副作用都是独立的(不会共享状态)。
3.2 实用案例:常用自定义Hooks
3.2.1 useFetch
:封装数据请求逻辑
// 自定义Hook:封装API请求逻辑
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 取消请求的控制器
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const res = await fetch(url, {signal: abortController.signal});
if (!res.ok) throw new Error('请求失败');
const json = await res.json();
setData(json);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') { // 忽略取消请求的错误
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// 清理函数:取消未完成的请求
return () => abortController.abort();
}, [url]); // url变化时重新请求
return {data, loading, error}; // 返回请求状态
};
// 使用自定义Hook
const UserList = () => {
const {data, loading, error} = useFetch('/api/users');
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
3.2.2 useLocalStorage
:封装本地存储逻辑
// 自定义Hook:同步state与localStorage
const useLocalStorage = (key, initialValue) => {
// 从localStorage读取初始值(首次渲染)
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (err) {
console.error('读取localStorage失败:', err);
return initialValue;
}
});
// 当value变化时,同步到localStorage
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error('写入localStorage失败:', err);
}
}, [key, value]);
return [value, setValue]; // 类似useState的返回值
};
// 使用:持久化存储用户偏好
const UserSettings = () => {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>当前主题:{theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
);
};
四、Hooks底层原理:为什么Hooks必须按顺序调用?
Hooks的设计依赖于**"调用顺序固定"**,理解其底层实现能帮你避免常见错误(如在条件语句中使用Hooks)。
4.1 依赖链表:Hooks的存储方式
React内部通过单向链表存储组件的Hooks状态,每个Hook对应链表中的一个节点,节点中存储状态值、更新函数、依赖等信息。
- 首次渲染时,React按Hooks的调用顺序创建链表节点(如
useState
→useEffect
→useState
对应三个节点); - 重新渲染时,React按相同顺序遍历链表,读取/更新每个节点的状态。
4.2 为什么不能在条件语句中使用Hooks?
若在条件语句中调用Hooks,会导致两次渲染的Hooks调用顺序不一致,破坏链表结构,React无法正确匹配状态:
// 错误示例:在条件中调用Hooks,导致顺序不一致
const BadExample = () => {
const [count, setCount] = useState(0);
if (count > 0) {
// 首次渲染count=0:不执行此Hook,链表长度1
// 后续count>0:执行此Hook,链表长度2 → 顺序混乱
const [name, setName] = useState('');
}
return <button onClick={() => setCount(c => c + 1)}>count: {count}</button>;
};
错误原因:首次渲染时链表只有count
一个节点,当count>0
时,第二次渲染会新增name
节点,导致后续Hooks(若有)的顺序与链表节点不匹配,React抛出错误。
4.3 为什么useEffect
的依赖要完整?
useEffect
的依赖数组用于判断是否需要重新执行副作用,其底层通过浅比较依赖项与上一次的快照:
- 若依赖项未变化(浅比较相等),复用上次的副作用(不执行);
- 若依赖项变化,执行清理函数(若有),再执行新的副作用。
若遗漏依赖,React会使用旧的依赖快照,导致副作用中访问的变量可能已过时(闭包陷阱)。
五、总结:Hooks的核心价值与最佳实践
Hooks的出现彻底改变了React的开发模式,其核心价值在于:
- 简化状态管理:函数组件无需转为类组件即可拥有状态;
- 逻辑复用更优雅:自定义Hooks替代高阶组件和Render Props,避免嵌套地狱;
- 代码更简洁:将相关逻辑(如状态+副作用)聚合在一处,而非分散在不同生命周期。
最佳实践:
- 遵循Hooks调用规则:只在函数组件或自定义Hooks中调用,不要在条件/循环中调用;
useEffect
依赖数组要完整,避免闭包陷阱;- 复杂状态用
useReducer
,跨组件数据用useContext
; - 提取重复逻辑为自定义Hooks,命名以
use
开头。
掌握Hooks不仅是记住API,更要理解其"状态与逻辑分离"的设计思想,写出更清晰、更可复用的React代码。