React Hooks 核心指南
0x01 什么是 Hooks?
Hooks 是 React 16.8 引入的特性,允许在函数组件中使用状态和其他 React 特性。Hooks 使得组件逻辑可复用,让函数组件具备类组件的能力。
0x02 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>
</div>
);
}
函数式更新
当新状态依赖旧状态时,使用函数式更新:
function Counter() {
const [count, setCount] = useState(0);
// 使用函数式更新,基于旧值计算新值
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
);
}
多个状态变量
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))}
placeholder="Age"
/>
</form>
);
}
对象状态
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
const updateEmail = (email) => {
setUser(prev => ({ ...prev, email }));
};
return (
<div>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>
</div>
);
}
惰性初始化
当状态初始值计算成本较高时,使用函数进行惰性初始化:
function ExpensiveComponent() {
// 只在首次渲染时计算初始值
const [data, setData] = useState(() => {
const initialData = computeExpensiveValue();
return initialData;
});
// 或者直接传入函数
const [config, setConfig] = useState(() => loadConfig());
}
0x03 useEffect —— 副作用处理
useEffect 用于处理副作用,如数据获取、订阅、手动 DOM 操作等。
基本用法
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 组件挂载后执行
fetchData()
.then(result => {
setData(result);
setLoading(false);
});
}, []); // 空依赖数组表示只在挂载时执行
if (loading) return <p>Loading...</p>;
return <div>{data}</div>;
}
依赖项
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// 当 userId 变化时重新执行
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // 依赖 userId
return <div>{user?.name}</div>;
}
清理副作用
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// 返回清理函数
return () => {
clearInterval(interval);
};
}, []); // 空依赖,只在挂载时设置定时器
return <p>Seconds: {seconds}</p>;
}
订阅模式
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 清理函数 - 移除订阅
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>Window width: {width}</p>;
}
条件执行
function Search({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const searchResults = performSearch(query);
setResults(searchResults);
}, [query]); // 当 query 变化时执行
return (
<ul>
{results.map(r => <li key={r.id}>{r.text}</li>)}
</ul>
);
}
0x04 useContext —— 跨组件通信
useContext 用于在组件树中共享数据,避免层层传递 props。
创建 Context
// ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export const LanguageContext = createContext('zh');
使用 Context
import { useContext } from 'react';
import { ThemeContext, LanguageContext } from './ThemeContext';
function App() {
return (
<ThemeContext.Provider value="dark">
<LanguageContext.Provider value="en">
<Header />
</LanguageContext.Provider>
</ThemeContext.Provider>
);
}
function Header() {
const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
return (
<header className={theme}>
<p>Language: {language}</p>
</header>
);
}
优化 Context 性能
import { useCallback, useMemo } from 'react';
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = useCallback((credentials) => {
// 登录逻辑
authenticate(credentials).then(setUser);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
// 使用 useMemo 缓存 context value,避免不必要的重渲染
const contextValue = useMemo(() => ({
user,
login,
logout
}), [user, login, logout]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
}
0x05 useRef —— 引用与可变值
useRef 用于访问 DOM 元素或存储可变值,且不会触发重新渲染。
访问 DOM
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
const clearInput = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
<button onClick={clearInput}>Clear</button>
</div>
);
}
存储可变值
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(0); // 可变值,不触发渲染
const increment = () => {
countRef.current += 1;
console.log('countRef:', countRef.current);
setCount(c => c + 1);
};
return (
<div>
<p>State: {count}</p>
<p>Ref: {countRef.current}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
跟踪前一个状态
function PreviousValue() {
const [value, setValue] = useState(0);
const prevValueRef = useRef();
useEffect(() => {
prevValueRef.current = value;
}, [value]);
return (
<div>
<p>Current: {value}</p>
<p>Previous: {prevValueRef.current}</p>
<button onClick={() => setValue(v => v + 1)}>Increment</button>
</div>
);
}
0x06 useMemo —— 值缓存
useMemo 用于缓存计算结果,避免不必要的重复计算。
基本用法
import { useMemo } from 'react';
function ExpensiveList({ items, filter }) {
// 只有 items 或 filter 变化时才重新计算
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.includes(filter)
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
对象优化
function Component({ user, theme }) {
// 避免每次渲染都创建新对象
const userInfo = useMemo(() => ({
name: user.name,
email: user.email,
avatar: user.avatar
}), [user.name, user.email, user.avatar]);
const style = useMemo(() => ({
color: theme === 'dark' ? '#fff' : '#000',
backgroundColor: theme === 'dark' ? '#333' : '#fff'
}), [theme]);
return <div style={style}>{userInfo.name}</div>;
}
0x07 useCallback —— 函数缓存
useCallback 用于缓存函数,避免函数重新创建。
基本用法
import { useCallback, memo } from 'react';
const List = memo(function List({ items, onItemClick }) {
console.log('List rendered');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function App() {
const [items] = useState([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]);
// 缓存函数,避免子组件不必要地重渲染
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return <List items={items} onItemClick={handleClick} />;
}
带依赖的回调
function App() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);
// 当 multiplier 变化时重新创建
const multiply = useCallback((value) => {
return value * multiplier;
}, [multiplier]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setMultiplier(m => m + 1)}>
Increase Multiplier
</button>
<p>Result: {multiply(10)}</p>
</div>
);
}
0x08 useReducer —— 复杂状态逻辑
useReducer 用于管理复杂的状态逻辑,类似于 Redux 的模式。
基本用法
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>
+1
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-1
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
);
}
复杂状态示例:待办事项
import { useReducer, createContext, useContext } from 'react';
// State 类型
interface Task {
id: number;
text: string;
completed: boolean;
}
interface State {
tasks: Task[];
filter: 'all' | 'active' | 'completed';
}
// Action 定义
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: number }
| { type: 'delete'; id: number }
| { type: 'setFilter'; filter: State['filter'] };
// Reducer
function tasksReducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return {
...state,
tasks: [
...state.tasks,
{
id: Date.now(),
text: action.text,
completed: false
}
]
};
case 'toggle':
return {
...state,
tasks: state.tasks.map(task =>
task.id === action.id
? { ...task, completed: !task.completed }
: task
)
};
case 'delete':
return {
...state,
tasks: state.tasks.filter(task => task.id !== action.id)
};
case 'setFilter':
return { ...state, filter: action.filter };
default:
return state;
}
}
// Context
const TasksContext = createContext<State | null>(null);
const TasksDispatchContext = createContext<React.Dispatch<Action> | null>(null);
// Provider
export function TasksProvider({ children }) {
const [state, dispatch] = useReducer(tasksReducer, {
tasks: [],
filter: 'all'
});
return (
<TasksContext.Provider value={state}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
// 自定义 Hooks
export function useTasks() {
const context = useContext(TasksContext);
if (!context) {
throw new Error('useTasks must be used within TasksProvider');
}
return context;
}
export function useTasksDispatch() {
const context = useContext(TasksDispatchContext);
if (!context) {
throw new Error('useTasksDispatch must be used within TasksProvider');
}
return context;
}
// 组件中使用
function TaskList() {
const { tasks, filter } = useTasks();
const dispatch = useTasksDispatch();
const filteredTasks = tasks.filter(task => {
if (filter === 'active') return !task.completed;
if (filter === 'completed') return task.completed;
return true;
});
return (
<ul>
{filteredTasks.map(task => (
<li key={task.id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => dispatch({ type: 'toggle', id: task.id })}
/>
<span style={{
textDecoration: task.completed ? 'line-through' : 'none'
}}>
{task.text}
</span>
<button onClick={() => dispatch({ type: 'delete', id: task.id })}>
Delete
</button>
</li>
))}
</ul>
);
}
异步操作
import { useReducer } from 'react';
interface State<T> {
data: T | null;
loading: boolean;
error: string | null;
}
type AsyncAction<T> =
| { type: 'pending' }
| { type: 'fulfilled'; payload: T }
| { type: 'rejected'; error: string };
function asyncReducer<T>(state: State<T>, action: AsyncAction<T>): State<T> {
switch (action.type) {
case 'pending':
return { data: null, loading: true, error: null };
case 'fulfilled':
return { data: action.payload, loading: false, error: null };
case 'rejected':
return { data: null, loading: false, error: action.error };
default:
return state;
}
}
function useAsync<T>(asyncFn: () => Promise<T>) {
const [state, dispatch] = useReducer(asyncReducer<T>, {
data: null,
loading: false,
error: null
});
const execute = async () => {
dispatch({ type: 'pending' });
try {
const data = await asyncFn();
dispatch({ type: 'fulfilled', payload: data });
} catch (error) {
dispatch({ type: 'rejected', error: error.message });
}
};
return { ...state, execute };
}
0x09 自定义 Hook
自定义 Hook 是复用状态逻辑的方式,本质是一个使用其他 Hook 的函数。
基本结构
import { useState, useEffect } from 'react';
// 自定义 Hook:以 use 开头的函数
function useWindowSize() {
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 size;
}
// 使用自定义 Hook
function App() {
const { width, height } = useWindowSize();
return (
<div>
<p>Window size: {width} x {height}</p>
</div>
);
}
复用状态逻辑
// useLocalStorage.ts
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue] as const;
}
// 使用
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [name, setName] = useLocalStorage('name', '');
return (
<div className={theme}>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
条件判断 Hook
// useOnClickOutside.ts
import { useEffect, RefObject } from 'react';
function useOnClickOutside<T extends HTMLElement>(
ref: RefObject<T>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// 使用
function Modal({ onClose }: { onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null);
useOnClickOutside(modalRef, onClose);
return (
<div ref={modalRef} className="modal">
<p>Modal Content</p>
</div>
);
}
0x10 Hooks 规则
规则一:只在顶层调用 Hook
// ❌ 错误:在条件语句中调用 Hook
function MyComponent() {
const [value, setValue] = useState(0);
if (condition) {
const [data, setData] = useState(null); // 错误!
}
// ...
}
// ✅ 正确:将条件放在 Hook 内部
function MyComponent() {
const [value, setValue] = useState(0);
const [data, setData] = useState(null);
if (condition) {
// 在这里使用 data
}
// ...
}
规则二:只在函数组件或自定义 Hook 中调用
// ❌ 错误:普通函数中调用 Hook
function regularFunction() {
const [value, setValue] = useState(0); // 错误!
}
// ✅ 正确:在组件或自定义 Hook 中调用
function MyComponent() {
const [value, setValue] = useState(0);
return <div>{value}</div>;
}
function useCustomHook() {
const [value, setValue] = useState(0);
return value;
}
使用 ESLint 插件
# 安装
npm install eslint-plugin-react-hooks --save-dev
# .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
};
0x11 常见问题
为什么 useEffect 会执行两次?
在开发模式下,React 会故意双重调用组件函数以检测副作用中的错误。这仅在开发环境出现,生产环境中不会发生。
如何正确设置依赖数组?
function Component({ id }) {
const [data, setData] = useState(null);
// ❌ 错误:忘记添加依赖
useEffect(() => {
fetchData(id).then(setData);
}, []); // 缺少 id
// ✅ 正确:添加所有依赖
useEffect(() => {
fetchData(id).then(setData);
}, [id]);
// ✅ 正确:使用函数避免依赖
useEffect(() => {
fetchData(id).then(setData);
}, [fetchData]); // 或者确保 fetchData 使用 useCallback
}
useState 和 useReducer 怎么选择?
- useState:适合简单的局部状态
- useReducer:适合复杂的状态逻辑,或多个子值相关的状态
// 使用 useState
const [state, setState] = useState({ a: 1, b: 2 });
setState(prev => ({ ...prev, b: prev.b + 1 }));
// 使用 useReducer
const [state, dispatch] = useReducer(reducer, { a: 1, b: 2 });
dispatch({ type: 'increment_b' });
参考
目录