React AJAX 语法知识点与案例详解
一、React AJAX 核心知识点
1. AJAX 在 React 中的基本概念
在 React 中,AJAX 请求通常在组件生命周期方法或 Hooks 中发起,用于从服务器获取数据并更新组件状态。
2. 常用的 AJAX 请求方式
2.1 Fetch API(原生)
- 现代浏览器内置的网络请求API
- 基于 Promise,支持 async/await
- 需要手动处理错误和 JSON 转换
2.2 Axios(第三方库)
- 基于 Promise 的 HTTP 客户端
- 自动转换 JSON 数据
- 支持请求/响应拦截器
- 支持取消请求
- 浏览器和 Node.js 环境都可用
2.3 XMLHttpRequest(传统,不推荐)
- 原生 JavaScript 对象
- 回调函数风格,代码较复杂
- 现代 React 项目中很少使用
3. 在 React 组件中使用 AJAX 的时机
3.1 Class Components
componentDidMount()
- 组件挂载后发起请求componentDidUpdate()
- 组件更新后根据条件发起请求componentWillUnmount()
- 清理定时器或取消未完成的请求
3.2 Function Components with Hooks
useEffect()
- 替代类组件的生命周期方法- 依赖数组控制何时发起请求
- 清理函数用于取消请求或清理资源
4. 状态管理
- 使用
useState
或this.state
存储加载状态、数据和错误信息 - 通常需要管理三种状态:
loading
: 请求是否正在进行data
: 请求成功后的数据error
: 请求失败的错误信息
5. 错误处理
- 网络错误处理
- 服务器返回错误状态码处理
- 数据格式错误处理
- 超时处理
6. 请求取消
- 防止组件卸载后更新状态导致的内存泄漏
- 使用 AbortController (Fetch) 或 CancelToken (Axios)
7. 并发请求处理
- Promise.all() 处理多个并行请求
- Promise.race() 获取最快响应的请求
8. 请求优化
- 防抖和节流
- 缓存机制
- 懒加载
二、详细案例代码
案例1:使用 Fetch API 的用户列表组件(函数组件)
jsx
import React, { useState, useEffect } from 'react';
import './UserList.css'; // 可选的样式文件
const UserList = () => {
// 定义状态
const [users, setUsers] = useState([]); // 存储用户数据
const [loading, setLoading] = useState(true); // 加载状态
const [error, setError] = useState(null); // 错误信息
const [controller, setController] = useState(null); // AbortController 实例
// 使用 useEffect 发起 AJAX 请求
useEffect(() => {
// 创建 AbortController 用于取消请求
const abortController = new AbortController();
setController(abortController);
// 定义获取用户数据的异步函数
const fetchUsers = async () => {
try {
// 更新加载状态
setLoading(true);
setError(null);
// 发起 Fetch 请求
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: abortController.signal // 关联 AbortController
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析 JSON 数据
const userData = await response.json();
// 更新状态
setUsers(userData);
setLoading(false);
} catch (err) {
// 检查是否是取消请求导致的错误
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
// 处理其他错误
setError(err.message);
setLoading(false);
console.error('获取用户数据失败:', err);
}
}
};
// 调用获取数据函数
fetchUsers();
// 清理函数:组件卸载时取消请求
return () => {
console.log('组件卸载,取消请求');
abortController.abort();
};
}, []); // 空依赖数组,只在组件挂载时执行一次
// 重新加载数据的函数
const handleReload = () => {
// 如果有正在进行的请求,先取消
if (controller) {
controller.abort();
}
// 重新设置状态并触发新的请求
setLoading(true);
setError(null);
setUsers([]);
// 创建新的 AbortController
const newController = new AbortController();
setController(newController);
// 重新获取数据
fetch('https://jsonplaceholder.typicode.com/users', {
signal: newController.signal
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
});
};
// 渲染加载状态
if (loading) {
return (
<div className="user-list-container">
<h2>用户列表</h2>
<div className="loading">加载中...</div>
</div>
);
}
// 渲染错误状态
if (error) {
return (
<div className="user-list-container">
<h2>用户列表</h2>
<div className="error">
<p>加载失败: {error}</p>
<button onClick={handleReload}>重试</button>
</div>
</div>
);
}
// 渲染正常数据
return (
<div className="user-list-container">
<h2>用户列表</h2>
<button onClick={handleReload} className="reload-btn">
刷新数据
</button>
<div className="user-grid">
{users.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p><strong>用户名:</strong> {user.username}</p>
<p><strong>邮箱:</strong> {user.email}</p>
<p><strong>电话:</strong> {user.phone}</p>
<p><strong>网站:</strong> {user.website}</p>
<div className="user-address">
<h4>地址:</h4>
<p>{user.address.street}, {user.address.suite}</p>
<p>{user.address.city}, {user.address.zipcode}</p>
</div>
<div className="user-company">
<h4>公司:</h4>
<p>{user.company.name}</p>
<p>{user.company.catchPhrase}</p>
</div>
</div>
))}
</div>
<p className="user-count">共 {users.length} 个用户</p>
</div>
);
};
export default UserList;
案例2:使用 Axios 的天气查询组件(函数组件)
jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './WeatherApp.css';
// 创建 axios 实例,设置默认配置
const api = axios.create({
baseURL: 'https://api.openweathermap.org/data/2.5',
timeout: 10000, // 10秒超时
});
// 你的 API key (实际使用时需要替换为真实的 key)
const API_KEY = 'your_api_key_here';
const WeatherApp = () => {
// 状态定义
const [city, setCity] = useState(''); // 输入的城市名
const [weatherData, setWeatherData] = useState(null); // 天气数据
const [loading, setLoading] = useState(false); // 加载状态
const [error, setError] = useState(null); // 错误信息
const [searchHistory, setSearchHistory] = useState([]); // 搜索历史
const [currentCity, setCurrentCity] = useState(''); // 当前显示的城市
// 组件挂载时,从 localStorage 读取搜索历史
useEffect(() => {
const savedHistory = localStorage.getItem('weatherSearchHistory');
if (savedHistory) {
setSearchHistory(JSON.parse(savedHistory));
}
}, []);
// 当搜索历史变化时,保存到 localStorage
useEffect(() => {
localStorage.setItem('weatherSearchHistory', JSON.stringify(searchHistory));
}, [searchHistory]);
// 搜索天气的函数
const searchWeather = async (searchCity) => {
if (!searchCity.trim()) {
setError('请输入城市名称');
return;
}
setLoading(true);
setError(null);
try {
// 并发请求:天气数据 + 预报数据
const [weatherResponse, forecastResponse] = await Promise.all([
api.get('/weather', {
params: {
q: searchCity,
appid: API_KEY,
units: 'metric', // 使用摄氏度
lang: 'zh_cn' // 中文描述
}
}),
api.get('/forecast', {
params: {
q: searchCity,
appid: API_KEY,
units: 'metric',
lang: 'zh_cn'
}
})
]);
// 处理天气数据
const currentWeather = {
city: weatherResponse.data.name,
country: weatherResponse.data.sys.country,
temperature: Math.round(weatherResponse.data.main.temp),
feelsLike: Math.round(weatherResponse.data.main.feels_like),
description: weatherResponse.data.weather[0].description,
icon: weatherResponse.data.weather[0].icon,
humidity: weatherResponse.data.main.humidity,
pressure: weatherResponse.data.main.pressure,
windSpeed: weatherResponse.data.wind.speed,
sunrise: new Date(weatherResponse.data.sys.sunrise * 1000),
sunset: new Date(weatherResponse.data.sys.sunset * 1000)
};
// 处理预报数据(只取未来5天中午12点的数据)
const dailyForecasts = [];
const forecastList = forecastResponse.data.list;
// 按天分组,取每天中午12点左右的数据
const groupedByDay = {};
forecastList.forEach(item => {
const date = new Date(item.dt * 1000);
const dayKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
// 优先选择中午12点左右的数据
if (!groupedByDay[dayKey] || Math.abs(date.getHours() - 12) < Math.abs(new Date(groupedByDay[dayKey].dt * 1000).getHours() - 12)) {
groupedByDay[dayKey] = item;
}
});
// 转换为数组(排除今天)
const today = new Date().toISOString().split('T')[0];
Object.values(groupedByDay)
.filter(item => item.dt_txt.split(' ')[0] !== today)
.slice(0, 5) // 只取未来5天
.forEach(item => {
dailyForecasts.push({
date: new Date(item.dt * 1000),
temperature: Math.round(item.main.temp),
description: item.weather[0].description,
icon: item.weather[0].icon
});
});
// 合并数据
const combinedData = {
current: currentWeather,
forecast: dailyForecasts
};
setWeatherData(combinedData);
setCurrentCity(searchCity);
// 更新搜索历史
setSearchHistory(prev => {
const newHistory = prev.filter(item => item !== searchCity);
newHistory.unshift(searchCity);
return newHistory.slice(0, 5); // 只保留最近5个
});
} catch (err) {
console.error('天气查询错误:', err);
if (err.code === 'ECONNABORTED') {
setError('请求超时,请稍后重试');
} else if (err.response) {
// 服务器返回了错误状态码
switch (err.response.status) {
case 404:
setError('未找到该城市,请检查城市名称');
break;
case 401:
setError('API密钥无效');
break;
case 429:
setError('请求过于频繁,请稍后再试');
break;
default:
setError(`服务器错误: ${err.response.status}`);
}
} else if (err.request) {
// 请求已发出但没有收到响应
setError('网络错误,请检查网络连接');
} else {
// 其他错误
setError('未知错误,请稍后重试');
}
} finally {
setLoading(false);
}
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault();
searchWeather(city);
};
// 从历史记录中选择城市
const handleHistoryClick = (historicalCity) => {
setCity(historicalCity);
searchWeather(historicalCity);
};
// 格式化日期
const formatDate = (date) => {
return date.toLocaleDateString('zh-CN', {
month: 'numeric',
day: 'numeric',
weekday: 'short'
});
};
// 格式化时间
const formatTime = (date) => {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className="weather-app">
<h1>天气预报</h1>
{/* 搜索表单 */}
<form onSubmit={handleSubmit} className="search-form">
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="请输入城市名称"
className="search-input"
/>
<button
type="submit"
disabled={loading}
className="search-button"
>
{loading ? '搜索中...' : '搜索'}
</button>
</form>
{/* 搜索历史 */}
{searchHistory.length > 0 && (
<div className="search-history">
<h3>搜索历史:</h3>
<div className="history-list">
{searchHistory.map((historicalCity, index) => (
<button
key={index}
onClick={() => handleHistoryClick(historicalCity)}
className="history-item"
>
{historicalCity}
</button>
))}
</div>
</div>
)}
{/* 错误信息 */}
{error && (
<div className="error-message">
{error}
</div>
)}
{/* 天气数据显示 */}
{weatherData && (
<div className="weather-display">
{/* 当前天气 */}
<div className="current-weather">
<div className="city-info">
<h2>{weatherData.current.city}, {weatherData.current.country}</h2>
<div className="date-time">
{new Date().toLocaleString('zh-CN')}
</div>
</div>
<div className="weather-main">
<img
src={`https://openweathermap.org/img/wn/${weatherData.current.icon}@2x.png`}
alt={weatherData.current.description}
className="weather-icon"
/>
<div className="temperature">
<span className="temp-value">{weatherData.current.temperature}°</span>
<span className="temp-unit">C</span>
</div>
<div className="weather-description">
{weatherData.current.description}
</div>
<div className="feels-like">
体感温度: {weatherData.current.feelsLike}°C
</div>
</div>
{/* 天气详情 */}
<div className="weather-details">
<div className="detail-item">
<span className="detail-label">湿度:</span>
<span className="detail-value">{weatherData.current.humidity}%</span>
</div>
<div className="detail-item">
<span className="detail-label">气压:</span>
<span className="detail-value">{weatherData.current.pressure} hPa</span>
</div>
<div className="detail-item">
<span className="detail-label">风速:</span>
<span className="detail-value">{weatherData.current.windSpeed} m/s</span>
</div>
<div className="detail-item">
<span className="detail-label">日出:</span>
<span className="detail-value">{formatTime(weatherData.current.sunrise)}</span>
</div>
<div className="detail-item">
<span className="detail-label">日落:</span>
<span className="detail-value">{formatTime(weatherData.current.sunset)}</span>
</div>
</div>
</div>
{/* 未来5天预报 */}
{weatherData.forecast.length > 0 && (
<div className="forecast">
<h3>未来5天预报</h3>
<div className="forecast-list">
{weatherData.forecast.map((day, index) => (
<div key={index} className="forecast-day">
<div className="forecast-date">{formatDate(day.date)}</div>
<img
src={`https://openweathermap.org/img/wn/${day.icon}.png`}
alt={day.description}
className="forecast-icon"
/>
<div className="forecast-temp">{day.temperature}°</div>
<div className="forecast-desc">{day.description}</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};
export default WeatherApp;
案例3:使用 Class Component 的商品管理组件
jsx
import React, { Component } from 'react';
import './ProductManager.css';
class ProductManager extends Component {
constructor(props) {
super(props);
// 初始化状态
this.state = {
products: [], // 商品列表
loading: false, // 加载状态
error: null, // 错误信息
newProduct: { // 新商品表单数据
name: '',
price: '',
description: '',
category: ''
},
editingProduct: null, // 正在编辑的商品
searchQuery: '', // 搜索关键词
categories: ['电子产品', '服装', '食品', '家居', '图书'], // 商品分类
page: 1, // 当前页码
totalPages: 1, // 总页数
limit: 10 // 每页显示数量
};
// 绑定 this
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSearchChange = this.handleSearchChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.handleAddProduct = this.handleAddProduct.bind(this);
this.handleEditProduct = this.handleEditProduct.bind(this);
this.handleUpdateProduct = this.handleUpdateProduct.bind(this);
this.handleDeleteProduct = this.handleDeleteProduct.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
}
// 组件挂载后获取商品数据
componentDidMount() {
this.fetchProducts();
}
// 当搜索关键词或页码变化时,重新获取数据
componentDidUpdate(prevProps, prevState) {
if (prevState.searchQuery !== this.state.searchQuery ||
prevState.page !== this.state.page) {
this.fetchProducts();
}
}
// 获取商品数据
fetchProducts = async () => {
this.setState({ loading: true, error: null });
try {
// 模拟 API 请求(实际项目中替换为真实 API)
const response = await fetch('/api/products', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// 添加查询参数
signal: this.abortController?.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.setState({
products: data.products || [],
totalPages: data.totalPages || 1,
loading: false
});
} catch (err) {
if (err.name !== 'AbortError') {
this.setState({
error: err.message,
loading: false
});
console.error('获取商品数据失败:', err);
}
}
};
// 创建 AbortController
createAbortController = () => {
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
};
// 输入框变化处理
handleInputChange = (e) => {
const { name, value } = e.target;
if (this.state.editingProduct) {
// 编辑模式
this.setState(prevState => ({
editingProduct: {
...prevState.editingProduct,
[name]: value
}
}));
} else {
// 添加模式
this.setState(prevState => ({
newProduct: {
...prevState.newProduct,
[name]: value
}
}));
}
};
// 搜索框变化处理
handleSearchChange = (e) => {
this.setState({
searchQuery: e.target.value,
page: 1 // 搜索时重置到第一页
});
};
// 页码变化处理
handlePageChange = (newPage) => {
this.setState({ page: newPage });
};
// 添加商品
handleAddProduct = async (e) => {
e.preventDefault();
const { newProduct } = this.state;
// 表单验证
if (!newProduct.name.trim() || !newProduct.price || !newProduct.category) {
this.setState({ error: '请填写完整商品信息' });
return;
}
this.setState({ loading: true, error: null });
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newProduct,
price: parseFloat(newProduct.price)
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const newProductData = await response.json();
// 更新状态
this.setState(prevState => ({
products: [newProductData, ...prevState.products],
newProduct: {
name: '',
price: '',
description: '',
category: ''
},
loading: false
}));
// 重新获取数据以更新分页
this.fetchProducts();
} catch (err) {
this.setState({
error: err.message,
loading: false
});
console.error('添加商品失败:', err);
}
};
// 编辑商品
handleEditProduct = (product) => {
this.setState({
editingProduct: { ...product },
newProduct: {
name: '',
price: '',
description: '',
category: ''
}
});
};
// 更新商品
handleUpdateProduct = async (e) => {
e.preventDefault();
const { editingProduct } = this.state;
if (!editingProduct.name.trim() || !editingProduct.price || !editingProduct.category) {
this.setState({ error: '请填写完整商品信息' });
return;
}
this.setState({ loading: true, error: null });
try {
const response = await fetch(`/api/products/${editingProduct.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...editingProduct,
price: parseFloat(editingProduct.price)
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const updatedProduct = await response.json();
// 更新状态
this.setState(prevState => ({
products: prevState.products.map(p =>
p.id === updatedProduct.id ? updatedProduct : p
),
editingProduct: null,
loading: false
}));
} catch (err) {
this.setState({
error: err.message,
loading: false
});
console.error('更新商品失败:', err);
}
};
// 删除商品
handleDeleteProduct = async (productId) => {
if (!window.confirm('确定要删除这个商品吗?')) {
return;
}
this.setState({ loading: true, error: null });
try {
const response = await fetch(`/api/products/${productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 更新状态
this.setState(prevState => ({
products: prevState.products.filter(p => p.id !== productId),
loading: false
}));
// 重新获取数据以更新分页
this.fetchProducts();
} catch (err) {
this.setState({
error: err.message,
loading: false
});
console.error('删除商品失败:', err);
}
};
// 取消编辑
cancelEdit = () => {
this.setState({
editingProduct: null,
newProduct: {
name: '',
price: '',
description: '',
category: ''
}
});
};
// 组件卸载时清理
componentWillUnmount() {
if (this.abortController) {
this.abortController.abort();
}
}
render() {
const {
products,
loading,
error,
newProduct,
editingProduct,
searchQuery,
categories,
page,
totalPages
} = this.state;
// 过滤搜索结果
const filteredProducts = products.filter(product =>
product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.category.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="product-manager">
<h1>商品管理</h1>
{/* 错误信息 */}
{error && (
<div className="error-message">
{error}
</div>
)}
{/* 搜索框 */}
<div className="search-bar">
<input
type="text"
value={searchQuery}
onChange={this.handleSearchChange}
placeholder="搜索商品名称或分类..."
className="search-input"
/>
</div>
{/* 添加/编辑商品表单 */}
<div className="product-form">
<h2>{editingProduct ? '编辑商品' : '添加新商品'}</h2>
<form onSubmit={editingProduct ? this.handleUpdateProduct : this.handleAddProduct}>
<div className="form-group">
<label>商品名称:</label>
<input
type="text"
name="name"
value={editingProduct ? editingProduct.name : newProduct.name}
onChange={this.handleInputChange}
required
/>
</div>
<div className="form-group">
<label>价格:</label>
<input
type="number"
name="price"
value={editingProduct ? editingProduct.price : newProduct.price}
onChange={this.handleInputChange}
min="0"
step="0.01"
required
/>
</div>
<div className="form-group">
<label>分类:</label>
<select
name="category"
value={editingProduct ? editingProduct.category : newProduct.category}
onChange={this.handleInputChange}
required
>
<option value="">请选择分类</option>
{categories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
</div>
<div className="form-group">
<label>描述:</label>
<textarea
name="description"
value={editingProduct ? editingProduct.description : newProduct.description}
onChange={this.handleInputChange}
rows="3"
/>
</div>
<div className="form-buttons">
{editingProduct ? (
<>
<button type="submit" disabled={loading}>
{loading ? '更新中...' : '更新商品'}
</button>
<button type="button" onClick={this.cancelEdit} className="cancel-btn">
取消
</button>
</>
) : (
<button type="submit" disabled={loading}>
{loading ? '添加中...' : '添加商品'}
</button>
)}
</div>
</form>
</div>
{/* 加载状态 */}
{loading && !error && (
<div className="loading-overlay">
<div className="loading-spinner"></div>
<p>处理中...</p>
</div>
)}
{/* 商品列表 */}
<div className="products-list">
<h2>商品列表 (共 {products.length} 个)</h2>
{filteredProducts.length === 0 ? (
<div className="no-products">
{searchQuery ? '没有找到匹配的商品' : '暂无商品数据'}
</div>
) : (
<table className="products-table">
<thead>
<tr>
<th>ID</th>
<th>商品名称</th>
<th>价格</th>
<th>分类</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{filteredProducts.map(product => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.name}</td>
<td>¥{parseFloat(product.price).toFixed(2)}</td>
<td>{product.category}</td>
<td>{product.description || '-'}</td>
<td className="action-buttons">
<button
onClick={() => this.handleEditProduct(product)}
className="edit-btn"
>
编辑
</button>
<button
onClick={() => this.handleDeleteProduct(product.id)}
className="delete-btn"
>
删除
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="pagination">
<button
onClick={() => this.handlePageChange(page - 1)}
disabled={page === 1 || loading}
className="page-btn"
>
上一页
</button>
<span className="page-info">
第 {page} 页 / 共 {totalPages} 页
</span>
<button
onClick={() => this.handlePageChange(page + 1)}
disabled={page === totalPages || loading}
className="page-btn"
>
下一页
</button>
</div>
)}
</div>
);
}
}
export default ProductManager;
案例4:自定义 Hook 封装 AJAX 逻辑
jsx
import { useState, useEffect, useCallback, useRef } from 'react';
// 自定义 Hook: useApi
const useApi = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [refreshCount, setRefreshCount] = useState(0);
// 使用 useRef 保存最新的 options,避免 useEffect 依赖问题
const optionsRef = useRef(options);
optionsRef.current = options;
// 使用 useRef 保存 AbortController
const abortControllerRef = useRef(null);
// 手动刷新函数
const refresh = useCallback(() => {
setRefreshCount(prev => prev + 1);
}, []);
// 取消请求函数
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
// 主要的请求函数
const fetchData = useCallback(async () => {
// 如果没有 URL,直接返回
if (!url) {
return;
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
setLoading(true);
setError(null);
try {
// 合并默认选项和传入选项
const config = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
...optionsRef.current,
signal: abortControllerRef.current.signal
};
const response = await fetch(url, config);
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 根据 Content-Type 决定如何解析响应
const contentType = response.headers.get('content-type');
let result;
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else {
result = await response.text();
}
setData(result);
setLoading(false);
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
setError(err.message);
setLoading(false);
console.error('API 请求失败:', err);
}
}
}, [url, refreshCount]);
// 使用 useEffect 执行请求
useEffect(() => {
fetchData();
// 清理函数
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [fetchData]); // fetchData 已经包含了所有依赖
return {
data,
loading,
error,
refresh,
cancel,
setData // 允许外部直接设置数据(用于乐观更新等场景)
};
};
// 自定义 Hook: usePostApi (专门用于 POST 请求)
const usePostApi = (url, initialData = null) => {
const [data, setData] = useState(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const abortControllerRef = useRef(null);
const post = useCallback(async (postData, config = {}) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setLoading(true);
setError(null);
setSuccess(false);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...config.headers
},
body: JSON.stringify(postData),
signal: abortControllerRef.current.signal,
...config
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
setData(result);
setSuccess(true);
setLoading(false);
return result;
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
setSuccess(false);
console.error('POST 请求失败:', err);
}
throw err; // 重新抛出错误,让调用者可以处理
}
}, [url]);
const reset = useCallback(() => {
setData(initialData);
setError(null);
setSuccess(false);
}, [initialData]);
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return {
data,
loading,
error,
success,
post,
reset
};
};
// 使用自定义 Hook 的组件示例
const UserProfile = ({ userId }) => {
// 使用 useApi Hook 获取用户数据
const {
data: user,
loading,
error,
refresh
} = useApi(userId ? `/api/users/${userId}` : null);
// 使用 usePostApi Hook 更新用户数据
const {
post: updateUser,
loading: updateLoading,
success: updateSuccess,
error: updateError,
reset: resetUpdate
} = usePostApi(`/api/users/${userId}`);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: ''
});
// 当用户数据加载完成后,初始化表单
useEffect(() => {
if (user) {
setFormData({
name: user.name || '',
email: user.email || '',
phone: user.phone || ''
});
}
}, [user]);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleUpdate = async (e) => {
e.preventDefault();
try {
await updateUser(formData);
setEditMode(false);
// 自动刷新用户数据
refresh();
} catch (err) {
// 错误已经在 usePostApi 中处理,这里可以添加额外的错误处理
console.error('更新失败:', err);
}
};
if (loading) {
return <div>加载中...</div>;
}
if (error) {
return (
<div>
<p>加载失败: {error}</p>
<button onClick={refresh}>重试</button>
</div>
);
}
if (!user) {
return <div>用户不存在</div>;
}
return (
<div className="user-profile">
<h2>用户资料</h2>
{updateSuccess && (
<div className="success-message">
更新成功!
<button onClick={resetUpdate}>×</button>
</div>
)}
{updateError && (
<div className="error-message">
更新失败: {updateError}
</div>
)}
{!editMode ? (
<div className="user-info">
<p><strong>姓名:</strong> {user.name}</p>
<p><strong>邮箱:</strong> {user.email}</p>
<p><strong>电话:</strong> {user.phone}</p>
<p><strong>注册时间:</strong> {new Date(user.createdAt).toLocaleString()}</p>
<button onClick={() => setEditMode(true)}>编辑资料</button>
<button onClick={refresh} disabled={loading}>
{loading ? '刷新中...' : '刷新'}
</button>
</div>
) : (
<form onSubmit={handleUpdate} className="edit-form">
<div className="form-group">
<label>姓名:</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label>邮箱:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label>电话:</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleInputChange}
/>
</div>
<div className="form-buttons">
<button type="submit" disabled={updateLoading}>
{updateLoading ? '更新中...' : '保存'}
</button>
<button type="button" onClick={() => {
setEditMode(false);
resetUpdate();
}}>
取消
</button>
</div>
</form>
)}
</div>
);
};
export { useApi, usePostApi, UserProfile };
三、最佳实践和注意事项
1. 错误处理最佳实践
- 始终处理网络错误和服务器错误
- 提供用户友好的错误信息
- 记录错误日志用于调试
2. 性能优化
- 使用防抖处理频繁的搜索请求
- 实现数据缓存避免重复请求
- 使用分页或懒加载处理大量数据
3. 安全考虑
- 验证用户输入
- 处理敏感数据(如 API keys)
- 使用 HTTPS
4. 代码组织
- 将 API 调用逻辑提取到单独的文件或自定义 Hook
- 使用环境变量管理 API 端点
- 保持组件职责单一
5. 测试
- 为 AJAX 调用编写单元测试
- 使用 Mock 数据进行测试
- 测试错误处理逻辑
这些案例涵盖了 React 中 AJAX 请求的主要使用场景和最佳实践,作为初学者,建议从简单的 Fetch API 开始,逐步学习更复杂的模式和优化技巧。