引言
React Hooks 是 React 16.8 版本引入的重要特性,它彻底改变了函数组件的开发方式,使得函数组件可以拥有状态(state)和生命周期等原本只有类组件才具备的能力。本文将深入探讨 React Hooks 的各个方面,从基础概念到高级用法,帮助你全面掌握这一现代 React 开发的核心技术。
1. React Hooks 基础概念
1.1 什么是 Hooks
Hooks 是 React 提供的一组函数,让你可以在函数组件中"钩入"React 的状态和生命周期等特性。它们以 use
开头命名,例如 useState
、useEffect
等。
1.2 为什么需要 Hooks
在 Hooks 出现之前,React 有两种创建组件的方式:
- 函数组件:简单但功能有限,无法使用状态和生命周期
- 类组件:功能完整但代码冗长,学习成本高,难以复用逻辑
Hooks 的出现解决了以下问题:
- 在不编写 class 的情况下使用 state 和其他 React 特性
- 更容易复用组件状态逻辑
- 简化复杂组件的理解和测试
- 避免类组件中 this 指向的问题
2. 基础 Hooks
2.1 useState
useState
是最基础的 Hook,用于在函数组件中添加状态。
jsx
import React, { useState } from 'react';
function Counter() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState 的使用要点:
- 数组解构 :
useState
返回一个数组,第一个元素是当前状态值,第二个元素是更新状态的函数 - 初始值 :
useState
的参数是状态的初始值 - 状态更新:调用更新函数会触发组件重新渲染
- 函数式更新:当新状态依赖于前一个状态时,可以传入函数:
jsx
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// 函数式更新,确保基于最新状态计算
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
2.2 useEffect
useEffect
用于处理副作用,如数据获取、订阅或手动修改 DOM。
jsx
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
// 更新文档标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 的清理机制:
jsx
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// 订阅好友状态
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 清理函数,在组件卸载时执行
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
useEffect 的依赖数组:
jsx
// 1. 没有依赖数组 - 每次渲染后都执行
useEffect(() => {
console.log('每次渲染后都执行');
});
// 2. 空依赖数组 - 只在挂载时执行一次
useEffect(() => {
console.log('只在挂载时执行');
}, []);
// 3. 有依赖项 - 依赖项变化时执行
useEffect(() => {
console.log('count 变化时执行');
}, [count]);
2.3 useContext
useContext
用于订阅 React context 的变更。
jsx
import React, { useContext } from 'react';
// 创建 Context
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 使用 useContext 获取 context 值
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme === 'dark' ? 'black' : 'white' }}>
I am styled by theme context!
</button>
);
}
3. 高级 Hooks
3.1 useReducer
useReducer
是 useState
的替代方案,适用于复杂的状态逻辑。
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 };
case 'reset':
return { count: action.payload };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
Reset
</button>
</>
);
}
3.2 useCallback
useCallback
用于优化性能,返回一个 memoized 回调函数。
jsx
import React, { useState, useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 使用 useCallback 缓存函数,避免子组件不必要的重新渲染
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
const handleNameChange = useCallback((e) => {
setName(e.target.value);
}, []);
return (
<div>
<Child onIncrement={handleIncrement} />
<input value={name} onChange={handleNameChange} />
</div>
);
}
const Child = React.memo(({ onIncrement }) => {
console.log('Child rendered');
return <button onClick={onIncrement}>Increment</button>;
});
3.3 useMemo
useMemo
用于优化性能,返回一个 memoized 值。
jsx
import React, { useState, useMemo } from 'react';
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// 使用 useMemo 缓存昂贵的计算结果
const expensiveValue = useMemo(() => {
console.log('执行昂贵的计算');
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);
return (
<div>
<p>Count: {count}</p>
<p>Expensive Value: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setItems([...items, { value: Math.random() }])}>
Add Item
</button>
</div>
);
}
3.4 useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数。
jsx
import React, { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 直接访问 DOM 节点
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
// 保存可变值的示例
function Timer() {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={stopTimer}>Stop Timer</button>
</div>
);
}
4. 自定义 Hooks
自定义 Hooks 是 React 中复用组件逻辑的常用方式。
4.1 创建自定义 Hook
jsx
import { useState, useEffect } from 'react';
// 自定义 Hook:获取当前窗口尺寸
function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
}
// 使用自定义 Hook
function ShowWindowDimensions() {
const { width, height } = useWindowDimensions();
return (
<div>
Window size: {width} x {height}
</div>
);
}
4.2 更复杂的自定义 Hook
jsx
import { useState, useEffect } from 'react';
// 自定义 Hook:数据获取
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 使用自定义 Hook
function UserProfile({ userId }) {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
5. Hooks 最佳实践
5.1 Hook 规则
- 只在最顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hooks
- 只在 React 函数组件中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hooks
jsx
// ✅ 正确:在顶层调用
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// ...
}, []);
// ...
}
// ❌ 错误:在条件语句中调用
function MyComponent({ condition }) {
if (condition) {
const [count, setCount] = useState(0); // 错误!
}
// ...
}
5.2 优化性能
jsx
import React, { useState, useCallback, useMemo } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 使用 useCallback 缓存函数
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []);
// 使用 useMemo 缓存计算结果
const expensiveCalculation = useMemo(() => {
// 模拟昂贵的计算
let result = 0;
for (let i = 0; i < count * 1000000; i++) {
result += i;
}
return result;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Calculation: {expensiveCalculation}</p>
<button onClick={handleIncrement}>Increment</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
</div>
);
}
5.3 错误处理
jsx
import { useState, useEffect } from 'react';
function useAsyncData(fetchFunction) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const result = await fetchFunction();
// 确保组件仍然挂载
if (isMounted) {
setData(result);
}
} catch (err) {
if (isMounted) {
setError(err);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
// 清理函数
return () => {
isMounted = false;
};
}, [fetchFunction]);
return { data, loading, error };
}
6. 实际应用场景
6.1 表单处理
jsx
import React, { useState } from 'react';
function useForm(initialValues, onSubmit) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: null
}));
}
};
const handleSubmit = (e) => {
e.preventDefault();
// 这里可以添加表单验证逻辑
onSubmit(values);
};
return {
values,
errors,
handleChange,
handleSubmit
};
}
function ContactForm() {
const { values, handleChange, handleSubmit } = useForm({
name: '',
email: '',
message: ''
}, (formData) => {
console.log('提交表单:', formData);
// 处理表单提交逻辑
});
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
/>
</div>
<div>
<label>Message:</label>
<textarea
name="message"
value={values.message}
onChange={handleChange}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
6.2 数据获取和缓存
jsx
import { useState, useEffect } from 'react';
// 简单的数据缓存实现
const cache = new Map();
function useCachedData(key, fetcher) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 检查缓存
if (cache.has(key)) {
setData(cache.get(key));
setLoading(false);
return;
}
const fetchData = async () => {
try {
setLoading(true);
const result = await fetcher();
cache.set(key, result);
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [key, fetcher]);
return { data, loading, error };
}
// 使用示例
function UserList() {
const { data: users, loading, error } = useCachedData(
'users',
() => fetch('/api/users').then(res => res.json())
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
6.3 动画和过渡效果
jsx
import { useState, useEffect, useRef } from 'react';
function useAnimation(initialValue, targetValue, duration = 300) {
const [currentValue, setCurrentValue] = useState(initialValue);
const startTimeRef = useRef(null);
const animationRef = useRef(null);
useEffect(() => {
if (startTimeRef.current === null) {
startTimeRef.current = performance.now();
}
const animate = (timestamp) => {
if (!startTimeRef.current) startTimeRef.current = timestamp;
const elapsed = timestamp - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
// 简单的线性插值
const newValue = initialValue + (targetValue - initialValue) * progress;
setCurrentValue(newValue);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
} else {
startTimeRef.current = null;
}
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [initialValue, targetValue, duration]);
return currentValue;
}
function AnimatedCounter() {
const [target, setTarget] = useState(0);
const animatedValue = useAnimation(0, target, 1000);
return (
<div>
<p>Animated Value: {Math.round(animatedValue)}</p>
<button onClick={() => setTarget(target + 100)}>
Increase Target
</button>
</div>
);
}
7. 常见陷阱和解决方案
7.1 无限循环问题
jsx
// ❌ 错误:会导致无限循环
function BadComponent() {
const [count, setCount] = useState(0);
// 每次渲染都会创建新对象,导致 useEffect 无限执行
const config = { theme: 'dark' };
useEffect(() => {
console.log('Effect executed');
// 一些副作用操作
}, [config]); // config 每次都是新对象
return <div>{count}</div>;
}
// ✅ 正确:使用 useMemo 或提取到组件外
function GoodComponent() {
const [count, setCount] = useState(0);
// 使用 useMemo 缓存对象
const config = useMemo(() => ({ theme: 'dark' }), []);
useEffect(() => {
console.log('Effect executed');
}, [config]);
return <div>{count}</div>;
}
7.2 闭包陷阱
jsx
// ❌ 错误:可能获取到过期的 state 值
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// 这里的 count 可能是过期的值
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // 依赖数组为空,count 不会更新
return <div>Count: {count}</div>;
}
// ✅ 正确:使用函数式更新
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// 使用函数式更新确保获取最新值
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Count: {count}</div>;
}
7.3 条件渲染中的 Hook
jsx
// ❌ 错误:Hook 的调用顺序不一致
function BadComponent({ showDetails }) {
const [count, setCount] = useState(0);
if (showDetails) {
// 条件下调用 Hook,可能导致顺序不一致
const [details, setDetails] = useState(null);
// ...
}
return <div>{count}</div>;
}
// ✅ 正确:始终调用所有 Hook
function GoodComponent({ showDetails }) {
const [count, setCount] = useState(0);
const [details, setDetails] = useState(null);
useEffect(() => {
if (showDetails) {
// 在 effect 中处理条件逻辑
fetchDetails().then(setDetails);
}
}, [showDetails]);
return <div>{count}</div>;
}
8. 测试 Hooks
8.1 使用 React Testing Library
jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { useState, useEffect } from 'react';
// 测试组件
function TestComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
// 测试用例
test('increments count when button is clicked', () => {
render(<TestComponent />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
fireEvent.click(screen.getByText('Increment'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
8.2 测试自定义 Hook
jsx
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
// 自定义 Hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 测试自定义 Hook
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('should reset counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});
9. 与其他状态管理方案的集成
9.1 与 Redux 集成
jsx
import { useSelector, useDispatch } from 'react-redux';
function TodoList() {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
);
}
9.2 与 Context 集成
jsx
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}
function Component() {
const { state, dispatch } = useApp();
// 使用 state 和 dispatch
}
10. 总结
React Hooks 彻底改变了 React 的开发方式,提供了更简洁、更灵活的状态和副作用管理方案。通过掌握基础 Hooks 和自定义 Hooks 的创建,我们可以构建更加可维护和可复用的组件。
关键要点回顾:
- 基础 Hooks :
useState
、useEffect
、useContext
是最常用的 Hooks - 高级 Hooks :
useReducer
、useCallback
、useMemo
、useRef
提供更多功能 - 自定义 Hooks:是复用逻辑的最佳方式
- 遵循规则:只在顶层调用 Hooks,只在 React 函数中调用 Hooks
- 性能优化 :合理使用
useCallback
和useMemo
- 避免陷阱:注意闭包、无限循环等问题
随着 React 生态的不断发展,Hooks 已经成为现代 React 开发的标准,掌握它们对于任何 React 开发者来说都是至关重要的技能。