React Hooks 详解:从基础到进阶 🚀
React Hooks 是 React 16.8 引入的重要特性,它允许开发者在函数组件中使用状态(State)和生命周期逻辑,而无需编写类组件。
Hooks 的核心思想:
- 把复杂的逻辑"封装"成可复用的函数。
- 让函数组件像类组件一样强大,但代码更简洁!
1. useState
:管理组件状态 💡
用途
useState
是最基础的 Hook,用于在函数组件中声明和更新状态变量。它返回一个包含当前状态值和更新函数的数组。
代码示例
jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 声明状态变量 count,初始值为 0
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
);
}
深入解析
-
状态更新是异步的 :
React 可能会将多个
setCount
调用合并以提高性能。例如,连续调用setCount(count + 1)
两次,最终只更新一次状态。 -
函数式更新 :
如果新状态依赖于旧状态,使用函数式更新可以确保获取最新的值。例如:
jsxsetCount(prevCount => prevCount + 1);
-
多个状态变量 :
可以在同一个组件中声明多个状态变量:
jsxconst [name, setName] = useState('John'); const [age, setAge] = useState(25);
-
不可变更新原则 :
状态更新必须通过
setState
函数,不能直接赋值(如count = 1
)。
注意事项
- 性能优化 :如果状态更新频繁且耗时,考虑使用
useReducer
替代useState
。
2. useEffect
:处理副作用 ⚙️
用途
useEffect
用于在组件渲染后执行副作用操作,例如数据请求、订阅事件、DOM 操作等。它结合了类组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
的功能。
代码示例
jsx
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(res => res.json())
.then(json => setData(json.title));
}, []); // 空数组表示只在组件挂载时执行一次
return (
<div>
{data ? <p>获取到的数据:{data}</p> : <p>加载中...</p>}
</div>
);
}
深入解析
-
依赖数组的作用:
- 空数组
[]
:副作用仅在组件挂载时执行一次。 - 包含变量的数组
[count]
:只有当count
变化时,副作用才会重新执行。 - 省略数组:副作用会在每次渲染后执行。
- 空数组
-
清理副作用 :
如果副作用需要清理(如取消订阅、移除事件监听器),返回一个清理函数:
jsxuseEffect(() => { const timer = setInterval(() => { console.log('定时器触发'); }, 1000); return () => clearInterval(timer); // 组件卸载时清除定时器 }, []);
-
同步 vs 异步 :
useEffect
中的副作用是异步执行的,不会阻塞 DOM 更新。如果需要立即操作 DOM,使用useLayoutEffect
。
注意事项
- 避免过度使用 :每个
useEffect
都会增加组件的复杂性,尽量合并相关逻辑。 - 依赖项缺失:如果依赖数组未包含所有相关变量,可能导致副作用无法正确执行或内存泄漏。
3. useReducer
:管理复杂状态逻辑 🧠
用途
useReducer
适用于复杂的状态逻辑(如表单验证、购物车计算)。它通过 reducer
函数统一管理状态更新,类似于 Redux 的模式。
代码示例
jsx
import React, { useReducer } from 'react';
// 定义 reducer 函数
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>当前计数:{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>增加</button>
<button onClick={() => dispatch({ type: 'decrement' })}>减少</button>
</div>
);
}
深入解析
-
纯函数原则 :
reducer
必须是一个纯函数,不能直接修改state
,而是返回新对象。 -
嵌套状态更新 :
对于嵌套对象的状态更新,推荐使用
immer
库简化代码。例如:jsximport produce from 'immer'; function reducer(state, action) { return produce(state, draft => { draft.nested.value = action.value; }); }
-
初始化状态 :
可以通过第三个参数
init
提供初始化函数:jsxconst [state, dispatch] = useReducer(reducer, initialState, init);
注意事项
- 避免过度设计 :对于简单的状态逻辑(如计数器),
useState
更直观。 - 调试困难 :复杂的
reducer
可能导致调试困难,建议拆分为多个小函数。
4. useRef
:访问 DOM 或存储可变值 🎯
用途
useRef
返回一个可变对象(.current
属性),常用于:
- 访问 DOM 元素
- 存储不随渲染变化的值(如定时器 ID、表单输入值)
代码示例
jsx
import React, { useRef } from 'react';
function InputFocus() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // 直接操作 DOM
};
return (
<div>
<input ref={inputRef} type="text" placeholder="点击按钮聚焦" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
深入解析
-
不会触发重新渲染 :
修改
.current
的值不会导致组件重新渲染。 -
跨渲染保持值 :
useRef
可以存储任何可变值(如count
、timeoutId
),适合需要跨渲染保持的场景。 -
与
useState
的对比:useRef
适合存储不需要触发渲染的值。useState
适合存储需要触发渲染的状态。
注意事项
- 避免滥用 :如果值需要触发渲染,优先使用
useState
。 - 内存泄漏风险:存储的 DOM 元素或事件监听器需在组件卸载时手动清理。
5. useContext
:跨组件共享状态 🌐
用途
useContext
用于在组件树中共享状态,避免通过 props
一层层传递(即"props drilling"问题)。
代码示例
jsx
import React, { createContext, useContext } from 'react';
// 创建 Context
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
function Page() {
const theme = useContext(ThemeContext);
return <p>当前主题:{theme}</p>;
}
深入解析
-
创建 Context :
使用
createContext
创建一个 Context 对象,提供默认值。 -
提供 Context 值 :
使用
Provider
组件包裹子组件树,并通过value
属性传递共享的状态。 -
消费 Context 值 :
使用
useContext
钩子获取 Context 的值,无需逐层传递props
。
注意事项
- 性能影响 :如果 Context 的值频繁变化,所有消费组件都会重新渲染。可以通过
useMemo
或useCallback
优化。 - 避免过度使用 :对于局部状态(如组件内部状态),优先使用
useState
。
6. useLayoutEffect
:同步 DOM 更新 ⚡️
用途
useLayoutEffect
与 useEffect
类似,但会在 DOM 更新之前同步执行,适合需要立即操作 DOM 的场景(如测量 DOM 宽度/高度)。
代码示例
jsx
import React, { useLayoutEffect, useRef } from 'react';
function MeasureBox() {
const boxRef = useRef(null);
useLayoutEffect(() => {
const width = boxRef.current.offsetWidth;
console.log('盒子宽度:', width);
}, []);
return <div ref={boxRef} style={{ width: '200px', height: '100px' }} />;
}
深入解析
-
同步执行 :
useLayoutEffect
是同步的,会阻塞浏览器的绘制,直到所有 DOM 更新完成。 -
适用场景:
- 需要测量 DOM 尺寸(如弹窗定位)。
- 需要同步更新样式(避免页面闪烁)。
-
清理副作用 :
同样支持返回清理函数,用于卸载时的资源释放。
注意事项
- 性能开销 :
useLayoutEffect
会阻塞渲染,谨慎使用以避免性能问题。 - 与
useEffect
的选择 :优先使用useEffect
,仅在需要同步操作 DOM 时使用useLayoutEffect
。
7. useMemo
:缓存计算结果 🧮
用途
useMemo
用于缓存计算结果,避免在每次组件渲染时重复执行昂贵的计算操作。它类似于类组件中的 shouldComponentUpdate
,但更灵活。
代码示例
jsx
import React, { useState, useMemo } from 'react';
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 使用 useMemo 缓存计算结果
const expensiveValue = useMemo(() => {
console.log('计算中...');
let result = 0;
for (let i = 0; i < 100000000; i++) {
result += i;
}
return result;
}, [count]); // 仅在 count 变化时重新计算
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加计数</button>
<p>文本:{text}</p>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>计算结果:{expensiveValue}</p>
</div>
);
}
深入解析
-
缓存机制 :
useMemo
会在依赖项([count]
)发生变化时重新执行计算,并缓存结果。如果依赖项未变化,则直接返回缓存值。 -
适用场景:
- 昂贵的计算:如大数据排序、复杂算法、频繁的字符串拼接等。
- 避免重复渲染:当某个值仅在特定条件下需要更新时。
-
注意事项:
- 避免滥用 :如果计算逻辑简单,直接计算比使用
useMemo
更高效。 - 依赖项管理:确保依赖数组正确,否则可能导致缓存失效或内存泄漏。
- 避免滥用 :如果计算逻辑简单,直接计算比使用
总结
Hook | 用途 | 适合场景 |
---|---|---|
useState |
管理简单状态 | 计数器、表单输入 |
useEffect |
处理副作用 | 数据请求、事件监听 |
useReducer |
管理复杂状态逻辑 | 表单验证、购物车 |
useRef |
访问 DOM 或存储可变值 | 输入框聚焦、定时器 ID |
useContext |
跨组件共享状态 | 主题切换、用户登录状态 |
useLayoutEffect |
同步 DOM 更新 | 测量 DOM 尺寸、同步样式更新 |
useMemo |
缓存计算结果 | 昂贵的计算、避免重复渲染 |
🚀 最后的小贴士:
- 多写代码:动手实践是掌握 Hooks 的唯一途径。
- 查文档:遇到问题时,先看官方文档,再翻社区文章。
- 看案例:通过真实项目理解技术如何落地。
🎉 学习就像练武功,坚持就是胜利!