React数据请求
下面,我们来系统的梳理关于 React 数据请求:fetch / axios + useEffect 的基本知识点:
一、React 数据交互概述
1.1 为什么需要数据请求?
现代 React 应用通常需要与后端 API 交互以获取或提交数据。数据请求是连接前端 UI 和后端服务的桥梁,实现动态内容展示、用户交互和数据持久化。
1.2 核心概念
- HTTP 请求:GET(获取数据)、POST(创建数据)、PUT(更新数据)、DELETE(删除数据)
- 异步操作:JavaScript 的非阻塞特性,不会阻塞 UI 渲染
- 副作用管理:数据请求属于副作用,需要在组件生命周期中合理管理
二、fetch API 详解
2.1 基础使用
// GET 请求
fetch('https://api.example.com/data').then(response => {if (!response.ok) {throw new Error('网络响应异常');}return response.json();}).then(data => console.log(data)).catch(error => console.error('请求失败:', error));// POST 请求
fetch('https://api.example.com/data', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({title: '新标题',content: '这是内容'})
})
.then(response => response.json())
.then(data => console.log(data));
2.2 fetch 特点
- 内置现代浏览器:无需额外安装
- Promise-based:支持链式调用
- 低层级 API:需要手动处理更多细节
- 默认不带 cookie:需要设置
credentials: 'include'
- 错误处理:不会 reject HTTP 错误状态(如 404、500)
三、axios 库详解
3.1 基础使用
npm install axios
import axios from 'axios';// GET 请求
axios.get('https://api.example.com/data').then(response => console.log(response.data)).catch(error => console.error('请求失败:', error));// POST 请求
axios.post('https://api.example.com/data', {title: '新标题',content: '这是内容'
}, {headers: {'Content-Type': 'application/json'}
})
.then(response => console.log(response.data));
3.2 axios 特点
- 功能更全面:自动转换 JSON 数据
- 错误处理更智能:HTTP 错误状态会触发 catch
- 拦截器支持:请求/响应拦截
- 取消请求:内置取消令牌
- 并发请求:
axios.all()
和axios.spread()
- 浏览器兼容性:支持旧版浏览器
3.3 fetch vs axios 对比
特性 | fetch | axios |
---|---|---|
安装 | 内置 | 需安装 |
语法简洁度 | ⭐⭐ | ⭐⭐⭐⭐ |
JSON 处理 | 需手动转换 | 自动转换 |
错误处理 | 需手动处理 HTTP 错误 | 自动处理 HTTP 错误 |
超时设置 | 不支持原生 | 支持 |
取消请求 | AbortController | CancelToken |
拦截器 | 不支持 | 支持 |
下载进度 | 支持 | 支持 |
上传进度 | 不支持 | 支持 |
浏览器兼容 | 现代浏览器 | 更广泛 |
四、useEffect 与数据请求
4.1 useEffect 基础
import { useEffect } from 'react';function DataComponent() {useEffect(() => {// 数据请求逻辑const fetchData = async () => {try {const response = await fetch('https://api.example.com/data');const data = await response.json();console.log(data);} catch (error) {console.error('请求失败:', error);}};fetchData();}, []); // 空依赖数组表示仅在组件挂载时执行return <div>数据组件</div>;
}
4.2 依赖数组详解
- 空数组
[]
:仅在组件挂载时执行一次 - 特定依赖
[dep1, dep2]
:依赖变化时重新执行 - 无依赖数组:每次渲染后都执行
// 根据 ID 获取数据
function UserProfile({ userId }) {const [user, setUser] = useState(null);useEffect(() => {const fetchUser = async () => {const response = await axios.get(`/api/users/${userId}`);setUser(response.data);};fetchUser();}, [userId]); // userId 变化时重新获取数据// 渲染用户信息...
}
五、完整数据请求模式
5.1 基础实现(状态管理)
import { useState, useEffect } from 'react';
import axios from 'axios';function DataFetcher() {const [data, setData] = useState([]);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const fetchData = async () => {try {setLoading(true);const response = await axios.get('https://api.example.com/data');setData(response.data);setError(null);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchData();}, []);if (loading) return <div>加载中...</div>;if (error) return <div>错误: {error}</div>;return (<ul>{data.map(item => (<li key={item.id}>{item.name}</li>))}</ul>);
}
5.2 进阶实现(取消请求)
import { useState, useEffect } from 'react';
import axios from 'axios';function SafeDataFetcher() {const [data, setData] = useState([]);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {// 创建取消令牌const cancelTokenSource = axios.CancelToken.source();const fetchData = async () => {try {setLoading(true);const response = await axios.get('https://api.example.com/data', {cancelToken: cancelTokenSource.token});setData(response.data);setError(null);} catch (err) {if (!axios.isCancel(err)) {setError(err.message);}} finally {if (!cancelTokenSource.token.reason) {setLoading(false);}}};fetchData();// 清理函数:取消未完成的请求return () => {cancelTokenSource.cancel('组件卸载,取消请求');};}, []);// 渲染逻辑...
}
5.3 使用 AbortController(fetch 取消)
useEffect(() => {const abortController = new AbortController();const fetchData = async () => {try {setLoading(true);const response = await fetch('https://api.example.com/data', {signal: abortController.signal});if (!response.ok) throw new Error('请求失败');const data = await response.json();setData(data);} catch (err) {if (err.name !== 'AbortError') {setError(err.message);}} finally {setLoading(false);}};fetchData();return () => {abortController.abort();};
}, []);
六、高级数据请求模式
6.1 并行请求
useEffect(() => {const fetchAllData = async () => {try {setLoading(true);// 使用 Promise.all 并行请求const [usersResponse, postsResponse] = await Promise.all([axios.get('/api/users'),axios.get('/api/posts')]);setUsers(usersResponse.data);setPosts(postsResponse.data);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchAllData();
}, []);
6.2 顺序请求
useEffect(() => {const fetchSequentialData = async () => {try {setLoading(true);// 先获取用户const userResponse = await axios.get(`/api/users/${userId}`);setUser(userResponse.data);// 再获取用户的帖子const postsResponse = await axios.get(`/api/users/${userId}/posts`);setPosts(postsResponse.data);} catch (err) {setError(err.message);} finally {setLoading(false);}};fetchSequentialData();
}, [userId]);
6.3 轮询数据
useEffect(() => {let isMounted = true;const intervalId = setInterval(() => {if (isMounted) {fetchData();}}, 5000); // 每5秒轮询一次// 初始加载fetchData();return () => {isMounted = false;clearInterval(intervalId);};async function fetchData() {try {const response = await axios.get('/api/live-data');setData(response.data);} catch (err) {console.error('轮询失败:', err);}}
}, []);
七、自定义 Hook 封装
7.1 基础数据请求 Hook
import { useState, useEffect } from 'react';
import axios from 'axios';function useFetch(url, options = {}) {const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const cancelTokenSource = axios.CancelToken.source();const fetchData = async () => {try {setLoading(true);const response = await axios({url,...options,cancelToken: cancelTokenSource.token});setData(response.data);setError(null);} catch (err) {if (!axios.isCancel(err)) {setError(err.message || '请求失败');}} finally {setLoading(false);}};fetchData();return () => {cancelTokenSource.cancel('请求取消');};}, [url, JSON.stringify(options)]); // 依赖项为 URL 和序列化的 optionsreturn { data, loading, error, refetch: fetchData };
}// 使用示例
function UserList() {const { data: users, loading, error } = useFetch('/api/users');if (loading) return <p>加载中...</p>;if (error) return <p>错误: {error}</p>;return (<ul>{users.map(user => (<li key={user.id}>{user.name}</li>))}</ul>);
}
7.2 高级 Hook(带缓存和刷新)
import { useState, useEffect, useCallback, useRef } from 'react';function useAdvancedFetch(url) {const [data, setData] = useState(null);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);const cache = useRef({});const fetchData = useCallback(async (abortController) => {if (cache.current[url]) {setData(cache.current[url]);return;}setLoading(true);setError(null);try {const signal = abortController ? abortController.signal : null;const response = await fetch(url, { signal });if (!response.ok) throw new Error('请求失败');const result = await response.json();cache.current[url] = result;setData(result);} catch (err) {if (err.name !== 'AbortError') {setError(err.message);}} finally {setLoading(false);}}, [url]);const refetch = useCallback(() => {// 清除缓存并重新获取delete cache.current[url];fetchData();}, [url, fetchData]);useEffect(() => {const abortController = new AbortController();fetchData(abortController);return () => {abortController.abort();};}, [fetchData]);return { data, loading, error, refetch };
}
八、实践与优化
8.1 性能优化策略
-
避免不必要请求:
useEffect(() => {// 只有当 userId 存在时才请求if (userId) {fetchUserData(userId);} }, [userId]);
-
防抖搜索请求:
useEffect(() => {const handler = setTimeout(() => {if (query.length > 2) {fetchResults(query);}}, 300); // 300ms 防抖return () => clearTimeout(handler); }, [query]);
-
数据缓存:
const cache = useRef({});useEffect(() => {if (cache.current[userId]) {setUserData(cache.current[userId]);return;}// 否则发起请求... }, [userId]);
8.2 错误处理最佳实践
-
统一错误处理:
try {// 请求逻辑 } catch (error) {if (axios.isCancel(error)) {console.log('请求已取消', error.message);} else if (error.response) {// 服务器返回错误状态 (4xx, 5xx)console.error('服务器错误:', error.response.status);} else if (error.request) {// 请求已发送但无响应console.error('无响应:', error.request);} else {// 其他错误console.error('请求设置错误:', error.message);} }
-
用户友好错误信息:
const getErrorMessage = (error) => {if (error.response) {switch (error.response.status) {case 401: return '未授权,请登录';case 403: return '拒绝访问';case 404: return '资源不存在';case 500: return '服务器错误';default: return `请求失败: ${error.response.status}`;}}return '网络错误,请重试'; };
8.3 安全实践
-
防止 XSS 攻击:
// 避免直接渲染未处理的数据 <div>{user.bio}</div> // 危险!// 使用 DOMPurify 或处理危险 HTML import DOMPurify from 'dompurify';<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(user.bio) }} />
-
保护敏感信息:
- 永远不要在前端存储 API 密钥
- 使用环境变量存储敏感配置
// .env 文件 REACT_APP_API_URL=https://api.example.com// 组件中使用 const apiUrl = process.env.REACT_APP_API_URL;
九、测试数据请求
9.1 使用 Jest 和 MSW 测试
npm install msw jest-fetch-mock --save-dev
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';const server = setupServer(rest.get('/api/users', (req, res, ctx) => {return res(ctx.json([{ id: 1, name: '用户1' },{ id: 2, name: '用户2' }]));})
);export default server;// 测试文件
import { render, screen, waitFor } from '@testing-library/react';
import server from './mocks/server';
import UserList from '../UserList';// 启动 API 模拟服务器
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());test('加载并显示用户列表', async () => {render(<UserList />);// 初始加载状态expect(screen.getByText(/加载中/i)).toBeInTheDocument();// 等待数据加载完成await waitFor(() => {expect(screen.getByText('用户1')).toBeInTheDocument();expect(screen.getByText('用户2')).toBeInTheDocument();});
});test('处理请求错误', async () => {// 覆盖默认处理程序以模拟错误server.use(rest.get('/api/users', (req, res, ctx) => {return res(ctx.status(500));}));render(<UserList />);await waitFor(() => {expect(screen.getByText(/请求失败/i)).toBeInTheDocument();});
});
十、常见问题与解决方案
10.1 无限请求循环
问题:依赖数组配置不当导致无限请求
// 错误示例:缺少依赖
useEffect(() => {fetchData(id); // id 变化但未在依赖数组中
}, []);// 错误示例:在 effect 中更新依赖项
const [data, setData] = useState([]);
useEffect(() => {fetchData().then(newData => setData(newData));
}, [data]); // data 变化会触发 effect
解决方案:
- 确保所有依赖项都正确声明
- 使用函数式更新避免不必要的依赖
useEffect(() => {fetchData().then(setData); // 正确:setData 是稳定的
}, [fetchData]); // 需要 useCallback 包装 fetchData
10.2 竞态条件
问题:请求返回顺序不确定导致数据错乱
useEffect(() => {let ignore = false;const fetchData = async () => {const result = await axios.get(`/api/data?id=${id}`);if (!ignore) {setData(result.data);}};fetchData();return () => {ignore = true;};
}, [id]);
10.3 内存泄漏
问题:组件卸载后更新状态
useEffect(() => {let isMounted = true;fetchData().then(result => {if (isMounted) {setData(result);}});return () => {isMounted = false;};
}, []);
十一、总结
关键点
- 使用 useEffect 管理副作用:确保数据请求与组件生命周期同步
- 正确处理加载和错误状态:提升用户体验
- 实现请求取消:避免组件卸载后更新状态
- 封装自定义 Hook:复用数据请求逻辑
- 编写测试:确保数据交互可靠性
选择
- 小型应用/简单请求:fetch + useEffect
- 中型应用/需要高级功能:axios + useEffect
- 大型应用/复杂状态管理:React Query 或 SWR