React Hooks 彻底改变了我们编写 React 组件的方式。自 React 16.8 起,Hooks 让你可以在函数组件中使用状态、生命周期方法、上下文等功能——无需类组件。本完整 React Hooks 指南通过实际示例介绍每个内置 Hook。
useState:管理组件状态
useState 是最基础的 Hook。它返回一个有状态的值和一个更新它的函数。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
</div>
);
}
// Lazy initialization — runs only on first render
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data') || '{}');
});
// Updating objects — always create a new object
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, name: 'Alice' }));
// Updating arrays — use spread or filter/map
const [items, setItems] = useState<string[]>([]);
setItems(prev => [...prev, 'new item']);
setItems(prev => prev.filter(item => item !== 'remove me'));useEffect:副作用与生命周期
useEffect 让你在函数组件中执行副作用,替代了 componentDidMount、componentDidUpdate 和 componentWillUnmount。
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
// Runs when userId changes (componentDidMount + componentDidUpdate)
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
// Cleanup function (componentWillUnmount)
return () => { cancelled = true; };
}, [userId]); // dependency array
return <div>{user?.name}</div>;
}
// Run once on mount
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []); // empty dependency array
// Run on every render (rarely needed)
useEffect(() => {
console.log('Component rendered');
}); // no dependency arrayuseContext:消费上下文
useContext 让你无需嵌套 Consumer 组件即可订阅 React 上下文。
import { createContext, useContext, useState } from 'react';
// 1. Create a context with a default value
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
// 2. Create a provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Consume context with useContext
function ThemeButton() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('Must be inside ThemeProvider');
return (
<button onClick={ctx.toggleTheme}>
Current: {ctx.theme}
</button>
);
}
// 4. Custom hook for cleaner usage
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}useMemo:缓存昂贵计算
useMemo 在重新渲染之间缓存昂贵计算的结果。仅在依赖项更改时重新计算。
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// Only recalculates when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize a sorted copy
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<ul>
{sortedItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// Do NOT overuse useMemo — only for truly expensive computations
// Simple operations do not need memoization
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);useCallback:缓存函数
useCallback 返回回调函数的缓存版本。在传递回调给优化的子组件时非常有用。
import { useCallback, useState, memo } from 'react';
// Child component wrapped in memo — only re-renders if props change
const SearchInput = memo(({ onSearch }: { onSearch: (q: string) => void }) => {
console.log('SearchInput rendered');
return <input onChange={e => onSearch(e.target.value)} />;
});
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Without useCallback, a new function is created every render
// causing SearchInput to re-render unnecessarily
const handleSearch = useCallback((q: string) => {
setQuery(q);
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
}, []); // stable reference
return (
<div>
<SearchInput onSearch={handleSearch} />
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</div>
);
}useRef:可变引用
useRef 返回一个可变 ref 对象,其 .current 属性在渲染之间持久存在且不会触发重新渲染。
import { useRef, useEffect, useState } from 'react';
function TextInputWithFocus() {
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // auto-focus on mount
}, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
// Track previous value
const prevTimeRef = useRef(time);
useEffect(() => { prevTimeRef.current = time; });
return (
<div>
<p>Time: {time}s (prev: {prevTimeRef.current}s)</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useReducer:复杂状态逻辑
useReducer 是 useState 的替代方案,用于管理复杂的状态逻辑。它遵循 Redux 的 dispatch action 模式。
import { useReducer } from 'react';
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return { count: 0, step: 1 };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}自定义 Hooks:可复用逻辑
自定义 Hooks 让你将组件逻辑提取为可复用的函数。自定义 Hook 是以 use 开头的函数。
// useLocalStorage — persist state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// useDebounce — debounce a rapidly changing value
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch — generic data fetching
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false); } })
.catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', '');
const debouncedName = useDebounce(name, 300);
const { data, loading } = useFetch<User[]>(`/api/search?q=${debouncedName}`);
}Hooks 规则
使用 Hooks 时必须遵循两条基本规则。
- 只在顶层调用 Hooks。不要在循环、条件或嵌套函数中调用 Hooks。
- 只从 React 函数中调用 Hooks。
// WRONG — Hook inside a condition
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // breaks Hook order
}
}
// CORRECT — condition inside the Hook
function Good({ isLoggedIn }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (isLoggedIn) fetchUser().then(setUser);
}, [isLoggedIn]);
}常见陷阱与解决方案
useEffect 中的过时闭包
在 useEffect 中引用状态但未将其加入依赖数组时,你会得到过时闭包。
// BUG: stale closure
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs 0 (stale!)
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // count is missing from dependencies
}
// FIX: use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // always uses latest value
}, 1000);
return () => clearInterval(id);
}, []); // safe with functional updateuseEffect 的无限循环
在 useEffect 中设置状态但没有正确的依赖项会导致无限重新渲染。
// BUG: infinite loop
useEffect(() => {
setCount(count + 1); // triggers re-render, which runs effect again
}); // no dependency array = runs every render
// FIX: add dependency array
useEffect(() => {
if (count < 10) setCount(count + 1);
}, [count]); // only runs when count changes对象/数组依赖项
对象和数组按引用比较,而非按值比较。
// BUG: new object every render
function App() {
const options = { page: 1, limit: 10 }; // new ref each render
useEffect(() => {
fetchData(options);
}, [options]); // runs every render!
}
// FIX: useMemo to stabilize the reference
function App() {
const options = useMemo(() => ({ page: 1, limit: 10 }), []);
useEffect(() => {
fetchData(options);
}, [options]); // stable reference
}常见问题
什么是 React Hooks?
React Hooks 是让你在函数组件中使用 React 特性(如状态、生命周期、上下文)的函数。
useMemo 和 useCallback 有什么区别?
useMemo 缓存计算值,useCallback 缓存函数引用。
什么时候应该用 useReducer 而不是 useState?
当状态逻辑复杂、涉及多个子值,或下一个状态依赖于前一个状态时。
能在类组件中使用 Hooks 吗?
不能,Hooks 只能在函数组件和自定义 Hooks 中使用。
如何避免 useEffect 的无限循环?
始终指定正确的依赖数组。空数组 [] 只在挂载时运行一次。
React Hooks 是现代 React 开发的基础。掌握这些核心 Hooks,你就能高效构建任何 UI。