·47 分钟
React Hooks 深入解析:从原理到实战
ReactHooks前端JavaScript前端
Hooks 基础概念
为什么需要 Hooks
类组件的问题:
- 逻辑复用困难(HOC、Render Props 嵌套)
- 复杂组件难以理解(生命周期分散)
- this 指向问题
Hooks 的优势:
- 逻辑复用更简单(自定义 Hooks)
- 代码更清晰(按功能组织)
- 无需类(函数组件更简洁)
Hooks 规则
jsx
// ✅ 规则1:只在顶层调用 Hooks
function MyComponent() {
const [count, setCount] = useState(0); // ✅ 顶层
if (count > 0) {
// ❌ 错误:不能在条件语句中调用
// const [name, setName] = useState('');
}
}
// ✅ 规则2:只在 React 函数组件或自定义 Hooks 中调用
function myHook() {
const [value, setValue] = useState(0); // ✅ 自定义 Hook
}
function regularFunction() {
// ❌ 错误:普通函数中不能调用 Hooks
// const [value, setValue] = useState(0);
}
useState 详解
基本用法
jsx
import { useState } from 'react';
function Counter() {
// 声明状态
const [count, setCount] = useState(0);
// 更新状态
const increment = () => {
setCount(count + 1);
};
// 函数式更新(推荐)
const incrementFunctional = () => {
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementFunctional}>+1 (Functional)</button>
</div>
);
}
惰性初始化
jsx
// ❌ 不推荐:每次渲染都会调用
const [state, setState] = useState(expensiveComputation());
// ✅ 推荐:只在首次渲染时调用
const [state, setState] = useState(() => {
return expensiveComputation();
});
// 实际应用
function TodoList() {
const [todos, setTodos] = useState(() => {
// 从 localStorage 读取
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
}
对象和数组状态
jsx
function Form() {
// 对象状态
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// ❌ 错误:会覆盖整个对象
const updateName = (name) => {
setUser({ name }); // email 和 age 丢失!
};
// ✅ 正确:展开运算符
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
// 数组状态
const [items, setItems] = useState([]);
// 添加元素
const addItem = (item) => {
setItems(prev => [...prev, item]);
};
// 删除元素
const removeItem = (index) => {
setItems(prev => prev.filter((_, i) => i !== index));
};
// 更新元素
const updateItem = (index, newItem) => {
setItems(prev => prev.map((item, i) =>
i === index ? newItem : item
));
};
}
useEffect 详解
基本用法
jsx
import { useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
// 每次渲染后执行
useEffect(() => {
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 清理函数
return () => {
clearInterval(timer);
};
}, []); // 空依赖数组:只在挂载和卸载时执行
return <p>Seconds: {seconds}</p>;
}
依赖数组
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ❌ 每次渲染都执行(无限循环)
useEffect(() => {
fetchUser(userId).then(setUser);
});
// ✅ 只在 userId 变化时执行
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// ✅ 只在挂载时执行
useEffect(() => {
console.log('Component mounted');
}, []);
}
// 多个 useEffect
function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// 用户数据
useEffect(() => {
fetchUser().then(setUser);
}, []);
// 帖子数据
useEffect(() => {
fetchPosts().then(setPosts);
}, []);
// 用户和帖子都加载后的操作
useEffect(() => {
if (user && posts.length > 0) {
console.log('Both loaded');
}
}, [user, posts]);
}
清理函数
jsx
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 添加事件监听
window.addEventListener('resize', handleResize);
// 清理:移除事件监听
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<p>Window size: {size.width} x {size.height}</p>
);
}
// 实际应用:订阅和取消订阅
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const unsubscribe = subscribeToRoom(roomId, (message) => {
setMessages(prev => [...prev, message]);
});
return () => {
unsubscribe();
};
}, [roomId]);
}
useCallback 详解
问题场景
jsx
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染都创建新函数
const handleClick = () => {
console.log('Clicked');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* Child 每次都会重新渲染 */}
<Child onClick={handleClick} />
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
使用 useCallback
jsx
function Parent() {
const [count, setCount] = useState(0);
// ✅ 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // 空依赖:函数永远不变
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* Child 只在 handleClick 变化时重新渲染 */}
<Child onClick={handleClick} />
</div>
);
}
// 依赖项
function SearchComponent() {
const [query, setQuery] = useState('');
// 依赖 query 的函数
const handleSearch = useCallback(() => {
search(query);
}, [query]); // query 变化时重新创建函数
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}
何时使用 useCallback
jsx
// ✅ 场景1:传递给 memo 组件
const MemoizedChild = React.memo(Child);
<MemoizedChild onClick={useCallback(() => {}, [])} />
// ✅ 场景2:作为其他 Hook 的依赖
useEffect(() => {
fetchData();
}, [useCallback(() => fetchData(), [])]);
// ❌ 不需要:普通事件处理
function Button() {
// 不需要 useCallback,因为没有性能问题
const handleClick = () => {
console.log('clicked');
};
return <button onClick={handleClick}>Click</button>;
}
useMemo 详解
问题场景
jsx
function ProductList({ products, filter }) {
// ❌ 每次渲染都重新计算
const filteredProducts = products.filter(p =>
p.category === filter
);
// ❌ 每次渲染都重新排序
const sortedProducts = filteredProducts.sort((a, b) =>
a.price - b.price
);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name}: ${p.price}</li>
))}
</ul>
);
}
使用 useMemo
jsx
function ProductList({ products, filter }) {
// ✅ 只在 products 或 filter 变化时重新计算
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
// ✅ 只在 filteredProducts 变化时重新排序
const sortedProducts = useMemo(() => {
return [...filteredProducts].sort((a, b) => a.price - b.price);
}, [filteredProducts]);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name}: ${p.price}</li>
))}
</ul>
);
}
// 实际应用:复杂计算
function Analytics({ data }) {
// ✅ 缓存复杂计算结果
const statistics = useMemo(() => {
const sum = data.reduce((acc, val) => acc + val, 0);
const avg = sum / data.length;
const max = Math.max(...data);
const min = Math.min(...data);
return { sum, avg, max, min };
}, [data]);
return (
<div>
<p>Sum: {statistics.sum}</p>
<p>Average: {statistics.avg}</p>
<p>Max: {statistics.max}</p>
<p>Min: {statistics.min}</p>
</div>
);
}
何时使用 useMemo
jsx
// ✅ 场景1:复杂计算
const result = useMemo(() => heavyComputation(data), [data]);
// ✅ 场景2:创建对象/数组(避免子组件重新渲染)
const options = useMemo(() => ({ key: value }), [value]);
// ✅ 场景3:作为其他 Hook 的依赖
const memoizedValue = useMemo(() => compute(a, b), [a, b]);
useEffect(() => {
doSomething(memoizedValue);
}, [memoizedValue]);
// ❌ 不需要:简单计算
function Double({ number }) {
// 不需要 useMemo,乘法很快
const doubled = number * 2;
return <p>{doubled}</p>;
}
useRef 详解
访问 DOM
jsx
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</div>
);
}
// 实际应用:表单验证
function Form() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
if (!nameRef.current.value) {
nameRef.current.focus();
return;
}
if (!emailRef.current.value) {
emailRef.current.focus();
return;
}
// 提交表单
};
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} placeholder="Name" />
<input ref={emailRef} placeholder="Email" />
<button type="submit">Submit</button>
</form>
);
}
保存可变值
jsx
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
// 保存前一个值
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<p>
Current: {count}, Previous: {prevCount}
</p>
);
}
自定义 Hooks
基本结构
jsx
// 自定义 Hook:以 "use" 开头
function useLocalStorage(key, initialValue) {
// 使用 useState
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
// 封装更新函数
const setValue = (value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 使用
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={e => setName(e.target.value)}
/>
);
}
常用自定义 Hooks
jsx
// 1. useFetch
function 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 response = await fetch(url, {
signal: abortController.signal
});
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => {
abortController.abort();
};
}, [url]);
return { data, loading, error };
}
// 2. useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 3. useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(
window.matchMedia(query).matches
);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event) => {
setMatches(event.matches);
};
mediaQuery.addEventListener('change', handler);
return () => {
mediaQuery.removeEventListener('change', handler);
};
}, [query]);
return matches;
}
// 4. useOnlineStatus
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
性能优化
React.memo
jsx
// 基本用法
const MemoizedComponent = React.memo(function MyComponent({ name }) {
console.log('Rendered');
return <p>Hello, {name}</p>;
});
// 自定义比较
const MemoizedComponent = React.memo(
function MyComponent({ items }) {
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
},
(prevProps, nextProps) => {
// 只在 items 长度变化时重新渲染
return prevProps.items.length === nextProps.items.length;
}
);
优化实践
jsx
function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// ✅ 缓存过滤后的列表
const filteredTodos = useMemo(() => {
return todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
}, [todos, filter]);
// ✅ 缓存回调函数
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<FilterButtons filter={filter} onFilterChange={setFilter} />
<TodoItems
todos={filteredTodos}
onToggle={handleToggle}
onDelete={handleDelete}
/>
</div>
);
}
const TodoItems = React.memo(({ todos, onToggle, onDelete }) => {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
常见陷阱
1. 闭包陷阱
jsx
function Counter() {
const [count, setCount] = useState(0);
// ❌ 问题:闭包捕获了旧的 count
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // 永远是 0 + 1
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖
// ✅ 解决:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 使用最新的值
}, 1000);
return () => clearInterval(timer);
}, []);
}
2. 依赖项遗漏
jsx
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// ❌ 问题:query 变化时不会重新搜索
useEffect(() => {
fetchResults(query).then(setResults);
}, []); // 遗漏了 query
// ✅ 正确:添加 query 到依赖数组
useEffect(() => {
fetchResults(query).then(setResults);
}, [query]);
}
3. 对象依赖
jsx
function Component({ config }) {
// ❌ 问题:config 对象每次都是新的
useEffect(() => {
doSomething(config);
}, [config]); // 每次渲染都执行
// ✅ 解决:使用 useMemo 缓存对象
const memoizedConfig = useMemo(() => config, [
config.url,
config.method
]);
useEffect(() => {
doSomething(memoizedConfig);
}, [memoizedConfig]);
}
总结
核心 Hooks
| Hook | 用途 | 依赖项 |
|---|---|---|
| useState | 状态管理 | 无 |
| useEffect | 副作用 | 依赖数组 |
| useCallback | 缓存函数 | 依赖数组 |
| useMemo | 缓存值 | 依赖数组 |
| useRef | 引用/可变值 | 无 |
使用原则
- 按功能组织:将相关的 Hooks 放在一起
- 提取自定义 Hooks:复用状态逻辑
- 优化性能:合理使用 memo、useCallback、useMemo
- 避免过度优化:只在真正需要时优化
学习资源
延伸阅读: