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 的用户列表组件(函数组件)

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 开始,逐步学习更复杂的模式和优化技巧。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
执笔论英雄4 小时前
【大模型学习cuda】入们第一个例子-向量和
学习
wdfk_prog4 小时前
[Linux]学习笔记系列 -- [drivers][input]input
linux·笔记·学习
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端