本文将深入讲解React Hooks的核心原理和高级用法,通过实战案例帮助你掌握Hooks的精髓,写出更优雅的React代码。
📋 目录
一、Hooks核心原理
1.1 为什么需要Hooks
❌ Class组件的痛点
jsx
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true };
}
componentDidMount() {
this.fetchUser();
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
// 问题:逻辑分散在多个生命周期中
// 问题:this指向问题
// 问题:难以复用状态逻辑
}
✅ Hooks的优势
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 逻辑集中,清晰明了
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 可复用的逻辑
const windowSize = useWindowSize();
// 没有this问题
// 更容易测试
}
1.2 Hooks工作原理
javascript
// React内部简化实现
let hooks = [];
let currentIndex = 0;
function useState(initialValue) {
const index = currentIndex;
// 首次渲染使用初始值,后续使用存储的值
if (hooks[index] === undefined) {
hooks[index] = initialValue;
}
const setState = (newValue) => {
hooks[index] = typeof newValue === 'function'
? newValue(hooks[index])
: newValue;
render(); // 触发重新渲染
};
currentIndex++;
return [hooks[index], setState];
}
// 每次渲染前重置index
function render() {
currentIndex = 0;
ReactDOM.render(<App />, root);
}
1.3 Hooks规则
javascript
// ❌ 错误:条件语句中使用Hook
function BadComponent({ condition }) {
if (condition) {
const [value, setValue] = useState(0); // ❌
}
}
// ❌ 错误:循环中使用Hook
function BadComponent({ items }) {
items.forEach(item => {
const [value, setValue] = useState(item); // ❌
});
}
// ✅ 正确:顶层调用
function GoodComponent({ condition }) {
const [value, setValue] = useState(0); // ✅
if (condition) {
// 在这里使用value
}
}
二、useState深度解析
2.1 基础用法与陷阱
❌ 常见错误:直接修改状态
jsx
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
// ❌ 直接修改数组
todos.push({ id: Date.now(), text });
setTodos(todos); // 不会触发更新!
};
}
✅ 正确做法:不可变更新
jsx
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
// ✅ 创建新数组
setTodos([...todos, { id: Date.now(), text }]);
};
const updateTodo = (id, text) => {
// ✅ map返回新数组
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text } : todo
));
};
const deleteTodo = (id) => {
// ✅ filter返回新数组
setTodos(todos.filter(todo => todo.id !== id));
};
}
2.2 函数式更新
jsx
function Counter() {
const [count, setCount] = useState(0);
// ❌ 闭包陷阱
const incrementThreeTimes = () => {
setCount(count + 1); // count = 0
setCount(count + 1); // count = 0
setCount(count + 1); // count = 0
// 结果:count = 1
};
// ✅ 函数式更新
const incrementThreeTimesCorrect = () => {
setCount(c => c + 1); // c = 0 → 1
setCount(c => c + 1); // c = 1 → 2
setCount(c => c + 1); // c = 2 → 3
// 结果:count = 3
};
}
2.3 惰性初始化
jsx
// ❌ 每次渲染都执行
function ExpensiveComponent() {
const [data, setData] = useState(
expensiveComputation() // 每次渲染都调用
);
}
// ✅ 惰性初始化,只执行一次
function ExpensiveComponent() {
const [data, setData] = useState(() => {
return expensiveComputation(); // 只在首次渲染调用
});
}
2.4 复杂状态管理
jsx
// 使用useReducer管理复杂状态
const initialState = {
loading: false,
error: null,
data: null
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
function DataFetcher({ url }) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetch(url)
.then(res => res.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, [url]);
return (
<div>
{state.loading && <Spinner />}
{state.error && <Error message={state.error} />}
{state.data && <DataDisplay data={state.data} />}
</div>
);
}
三、useEffect完全指南
3.1 依赖数组详解
jsx
// 1. 无依赖数组:每次渲染都执行
useEffect(() => {
console.log('每次渲染都执行');
});
// 2. 空依赖数组:只在挂载时执行
useEffect(() => {
console.log('只在挂载时执行');
}, []);
// 3. 有依赖:依赖变化时执行
useEffect(() => {
console.log('userId变化时执行');
}, [userId]);
3.2 清理函数
❌ 内存泄漏
jsx
function UserStatus({ userId }) {
const [status, setStatus] = useState('offline');
useEffect(() => {
// 订阅用户状态
const subscription = subscribeToUserStatus(userId, setStatus);
// ❌ 没有清理,组件卸载后仍在订阅
}, [userId]);
}
✅ 正确清理
jsx
function UserStatus({ userId }) {
const [status, setStatus] = useState('offline');
useEffect(() => {
const subscription = subscribeToUserStatus(userId, setStatus);
// ✅ 返回清理函数
return () => {
subscription.unsubscribe();
};
}, [userId]);
}
3.3 竞态条件处理
jsx
// ❌ 竞态条件问题
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(setResults);
// 如果query快速变化,可能显示旧结果
}, [query]);
}
// ✅ 使用标志位解决
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let cancelled = false;
fetchResults(query).then(data => {
if (!cancelled) {
setResults(data);
}
});
return () => {
cancelled = true;
};
}, [query]);
}
// ✅ 使用AbortController
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(res => res.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort();
}, [query]);
}
3.4 useLayoutEffect vs useEffect
jsx
// useEffect:异步执行,不阻塞渲染
useEffect(() => {
// DOM已经渲染到屏幕
// 适合:数据获取、订阅、日志
}, []);
// useLayoutEffect:同步执行,阻塞渲染
useLayoutEffect(() => {
// DOM已更新但未渲染到屏幕
// 适合:DOM测量、同步动画
const height = ref.current.getBoundingClientRect().height;
setHeight(height);
}, []);
四、useRef与useImperativeHandle
4.1 useRef的多种用途
jsx
function MultipleRefUsages() {
// 1. 访问DOM元素
const inputRef = useRef(null);
// 2. 存储可变值(不触发重渲染)
const renderCount = useRef(0);
// 3. 保存上一次的值
const prevValue = useRef(null);
useEffect(() => {
renderCount.current++;
console.log(`渲染次数: ${renderCount.current}`);
});
const focusInput = () => {
inputRef.current.focus();
};
return <input ref={inputRef} />;
}
4.2 forwardRef与useImperativeHandle
jsx
// 子组件暴露方法给父组件
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
const [value, setValue] = useState('');
// 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
setValue('');
},
getValue: () => value
}), [value]);
return (
<input
ref={inputRef}
value={value}
onChange={e => setValue(e.target.value)}
/>
);
});
// 父组件使用
function Parent() {
const inputRef = useRef();
const handleSubmit = () => {
console.log(inputRef.current.getValue());
inputRef.current.clear();
};
return (
<>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<button onClick={handleSubmit}>提交</button>
</>
);
}
五、性能优化Hooks
5.1 useMemo缓存计算结果
jsx
// ❌ 每次渲染都重新计算
function ExpensiveList({ items, filter }) {
const filteredItems = items.filter(item =>
item.name.includes(filter)
); // 每次渲染都执行
return <List items={filteredItems} />;
}
// ✅ 缓存计算结果
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]); // 只在依赖变化时重新计算
return <List items={filteredItems} />;
}
5.2 useCallback缓存函数
jsx
// ❌ 每次渲染创建新函数,导致子组件重渲染
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('clicked');
}; // 每次渲染都是新函数
return <Child onClick={handleClick} />;
}
// ✅ 缓存函数引用
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 函数引用保持不变
return <Child onClick={handleClick} />;
}
// 配合React.memo使用
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
5.3 useTransition并发特性
jsx
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 紧急更新:输入框立即响应
setQuery(e.target.value);
// 非紧急更新:可以被中断
startTransition(() => {
setResults(searchItems(e.target.value));
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList results={results} />
</>
);
}
5.4 useDeferredValue延迟值
jsx
function SearchResults({ query }) {
// 延迟更新,优先保证输入响应
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
return searchItems(deferredQuery);
}, [deferredQuery]);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ResultList results={results} />
</div>
);
}
六、自定义Hooks实战
6.1 useLocalStorage
jsx
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// 使用
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
当前主题: {theme}
</button>
);
}
6.2 useFetch
jsx
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const optionsRef = useRef(options);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
...optionsRef.current,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 使用
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return <List items={users} />;
}
6.3 useDebounce
jsx
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
// 执行搜索
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
);
}
6.4 useClickOutside
jsx
function useClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// 使用:点击外部关闭下拉菜单
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef();
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <DropdownMenu />}
</div>
);
}
6.5 useIntersectionObserver
jsx
function useIntersectionObserver(options = {}) {
const [entry, setEntry] = useState(null);
const [node, setNode] = useState(null);
const observer = useRef(null);
useEffect(() => {
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver(
([entry]) => setEntry(entry),
options
);
if (node) {
observer.current.observe(node);
}
return () => {
if (observer.current) {
observer.current.disconnect();
}
};
}, [node, options.threshold, options.root, options.rootMargin]);
return [setNode, entry];
}
// 使用:图片懒加载
function LazyImage({ src, alt }) {
const [ref, entry] = useIntersectionObserver({
threshold: 0.1,
rootMargin: '100px'
});
const isVisible = entry?.isIntersecting;
return (
<div ref={ref}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" />
)}
</div>
);
}
📊 Hooks使用总结
| Hook | 用途 | 注意事项 |
|---|---|---|
| useState | 状态管理 | 不可变更新 |
| useEffect | 副作用处理 | 清理函数、依赖数组 |
| useRef | DOM引用/可变值 | 不触发重渲染 |
| useMemo | 缓存计算结果 | 避免过度使用 |
| useCallback | 缓存函数 | 配合memo使用 |
| useReducer | 复杂状态 | 替代多个useState |
| useContext | 跨组件通信 | 避免过度使用 |
💡 总结
React Hooks的核心要点:
- ✅ 遵守Hooks规则:顶层调用,不在条件/循环中使用
- ✅ 正确处理依赖:useEffect依赖数组要完整
- ✅ 避免闭包陷阱:使用函数式更新
- ✅ 合理性能优化:useMemo/useCallback不要滥用
- ✅ 封装自定义Hooks:提高代码复用性
掌握Hooks,让你的React代码更简洁、更易维护!
💬 如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论~