当前位置: 首页 > news >正文

React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)

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. 状态管理

  • 使用 useStatethis.state 存储加载状态、数据和错误信息
  • 通常需要管理三种状态:
    • loading: 请求是否正在进行
    • data: 请求成功后的数据
    • error: 请求失败的错误信息

5. 错误处理

  • 网络错误处理
  • 服务器返回错误状态码处理
  • 数据格式错误处理
  • 超时处理

6. 请求取消

  • 防止组件卸载后更新状态导致的内存泄漏
  • 使用 AbortController (Fetch) 或 CancelToken (Axios)

7. 并发请求处理

  • Promise.all() 处理多个并行请求
  • Promise.race() 获取最快响应的请求

8. 请求优化

  • 防抖和节流
  • 缓存机制
  • 懒加载

二、详细案例代码

案例1:使用 Fetch API 的用户列表组件(函数组件)

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([]);// 创建新的 AbortControllerconst 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 的天气查询组件(函数组件)

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));}}, []);// 当搜索历史变化时,保存到 localStorageuseEffect(() => {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"><inputtype="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) => (<buttonkey={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 的商品管理组件

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              // 每页显示数量};// 绑定 thisthis.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);}}};// 创建 AbortControllercreateAbortController = () => {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"><inputtype="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><inputtype="text"name="name"value={editingProduct ? editingProduct.name : newProduct.name}onChange={this.handleInputChange}required/></div><div className="form-group"><label>价格:</label><inputtype="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><selectname="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><textareaname="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 逻辑

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 保存 AbortControllerconst 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;}// 创建新的 AbortControllerabortControllerRef.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><inputtype="text"name="name"value={formData.name}onChange={handleInputChange}required/></div><div className="form-group"><label>邮箱:</label><inputtype="email"name="email"value={formData.email}onChange={handleInputChange}required/></div><div className="form-group"><label>电话:</label><inputtype="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 开始,逐步学习更复杂的模式和优化技巧。


文章转载自:

http://a4AgAfga.ndxrm.cn
http://a2iNmpQZ.ndxrm.cn
http://lQ9X55u0.ndxrm.cn
http://bqs38kMv.ndxrm.cn
http://9TpvL5Gf.ndxrm.cn
http://GSbPTZUB.ndxrm.cn
http://ViX6fqgf.ndxrm.cn
http://bJVMMPAk.ndxrm.cn
http://VRaOfNkZ.ndxrm.cn
http://O73g7J6G.ndxrm.cn
http://X552AvPN.ndxrm.cn
http://wEsP1S2L.ndxrm.cn
http://n1ssI8md.ndxrm.cn
http://0DE50D6F.ndxrm.cn
http://fNmrLNwy.ndxrm.cn
http://wE1LneVk.ndxrm.cn
http://qHgfCzam.ndxrm.cn
http://Q149pxMA.ndxrm.cn
http://zWfNU6m7.ndxrm.cn
http://3bIjBZe3.ndxrm.cn
http://46wJOg2U.ndxrm.cn
http://8loHNwfZ.ndxrm.cn
http://KAcE746X.ndxrm.cn
http://ggCbN6qH.ndxrm.cn
http://BlQ1W4KD.ndxrm.cn
http://kNSwjdhO.ndxrm.cn
http://wqDbxKtl.ndxrm.cn
http://BeC9vPEh.ndxrm.cn
http://GDmwW4AZ.ndxrm.cn
http://qliHQKth.ndxrm.cn
http://www.dtcms.com/a/380040.html

相关文章:

  • Go语言详细指南:特点、应用场景与开发工具
  • vue el-cascader级联选择器-地区三级选择问题记录
  • 《机器人抓取:从经典到现代的综述》内容的提取和凝练:
  • 【ZEGO即构开发者日报】微信公众号上线“智能回复”功能;2025年8月中国应用/游戏厂商出海收入Top30榜;土耳其宣布将封禁29款社交/社媒应用……
  • qt QAreaLegendMarker详解
  • #C语言——刷题攻略:牛客编程入门训练(十三):循环输出图形(二)、一维数组(一),轻松拿捏!
  • Nginx服务——安装与搭建
  • 远程真机调试支持网络多线路切换,让自助兼容性测试更流畅
  • AI Agent工作流实用手册:5种常见模式的实现与应用,助力生产环境稳定性
  • 前端渲染技术全解析:SSR、SSG、CSR 有什么区别?
  • html css js网页制作成品——HTML+CSS娃娃店网页设计(4页)附源码
  • mac本地安装mysql
  • 使用android studio分析cpu开销
  • Android Studio如何开启离线编译模式
  • CSS 动画实战:实现电商中“加入购物车”的抛物线效果
  • Terraform整合到GitLab+Jenkins工具链
  • android studio 断点无效
  • Prompt技术深度解析:从基础原理到前沿应用的全面指南
  • WPF报错 XDG000 Windows Presentation Foundation (WPF) 项目中不支持 Application
  • Docker的使用及核心命令
  • Jmeter测试
  • 神经网络基本概念
  • 【打包app】uniapp打包ios端和安卓端app
  • 【LeetCode 每日一题】3000. 对角线最长的矩形的面积
  • 制造业档案管理混乱:档案宝如何破解?
  • 第4周 数组的概念和常见操作
  • vue,uniapp 实现卷帘对比效果
  • 鸿蒙 NEXT UI 性能优化实战:打造流畅用户界面的关键策略
  • 使用UniApp实现一个AI对话页面
  • 智能科技与搜索引擎优化关键词的新契机