如何在 React 中向 API 发出异步请求
这是 React 新手在开发新项目时经常遇到的问题。我会在这里向你展示如何操作,以及一个可以帮助你编写更好、更简洁的代码(附带测试!)的方法。
假设我们正在开发一个新的博客应用,它将根据 API 的响应渲染一个简单的文章列表。通常我们会这样写:
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Post } from '../../types/post';
import Pagination from '../Pagination/Pagination';
import PostCard from '../PostCard/PostCard';
const DirBlogPosts: React.FC = () => {
const [page, setPage] = useState<number>(1);
const [posts, setPosts] = useState<Array<Post>>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
useEffect(() => {
(async () => {
try {
setIsLoading(true);
const { data } = await axios.get<Array<Post>>('https://example.com/posts', {
params: { page },
});
setPosts(data);
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
})();
}, [page]);
if (isLoading) {
return <p>Loading posts...</p>;
}
if (isError) {
return <p>There was an error trying to load the posts.</p>;
}
return (
<div>
{posts.map((post) => (
<PostCard post={post} />
))}
<Pagination page={page} onChangePage={setPage} />
</div>
);
};
export default DirBlogPosts;
这里我们有状态page
、和。这些状态在组件首次渲染或 发生更改时posts
更新。isLoading
isError
page
你能看出这里的问题吗?
- 我们的组件内部有所有的获取逻辑;
- 我们需要手动控制许多状态;
- 创建自动化测试很困难。
但我们可以尝试采用不同的方法并创建更清晰的代码。
构建您的服务
首先,利用 Typescript 的特性,我们来定义一下什么是帖子:
// src/types/post.ts
export type Post = {
id: number;
title: string;
imageUrl: string;
content: string;
};
该帖子基本上是一个具有id
、、和的对象title
。imageUrl
content
现在我们可以创建“列表帖子服务”的定义:
// src/services/definitions/list-posts-service.ts
import { Post } from '../../types/post';
export interface ListPostsService {
list(params: ListPostsService.Params): Promise<ListPostsService.Result>;
}
export namespace ListPostsService {
export type Params = {
page?: number;
};
export type Result = Array<Post>;
}
这里我们定义“列表帖子服务”实现应该有一个名为的方法list
,它将接收定义的参数并返回定义的结果。
为什么我们要为此创建一个界面?
答案很简单:我们的组件将接收并执行此服务。组件甚至不需要知道您将使用 Axios 还是 Fetch。假设您的组件是不可知的。稍后您可能需要将 Axios 更改为 Fetch,甚至使用 Redux。
那么,让我们构建我们的 Axios 服务实现:
// src/services/implementation/axios-list-posts-service.ts
import { AxiosInstance } from 'axios';
import { Post } from '../../types/post';
import { ListPostsService } from '../definitions/list-posts-service';
export default class AxiosListPostsService implements ListPostsService {
constructor(private readonly axiosInstance: AxiosInstance) {}
async list(params: ListPostsService.Params): Promise<ListPostsService.Result> {
const { data } = await this.axiosInstance.get<Array<Post>>('/posts', {
params: { page: params.page },
});
return data;
}
}
这是我们使用 Axios 的实现。我们需要在构造函数中使用 Axios 实例,并在方法中list
向端点发出请求/posts
。
由于我们已经在开发这项服务,因此我们还要创建一个模拟版本以供测试使用:
import faker from 'faker';
import lodash from 'lodash';
import { ListPostsService } from './list-posts-service';
export const mockListPostsServicesResult = (): ListPostsService.Result => {
return lodash.range(10).map((id) => ({
id,
title: faker.lorem.words(),
content: faker.lorem.paragraphs(),
imageUrl: faker.internet.url(),
}));
};
export class ListPostsServiceSpy implements ListPostsService {
params: ListPostsService.Params;
result: ListPostsService.Result = mockListPostsServicesResult();
async list(params: ListPostsService.Params): Promise<ListPostsService.Result> {
this.params = params;
return this.result;
}
}
我们只需要将参数和模拟结果存储在类中,以便稍后使用 Jest 进行测试。对于模拟数据,我喜欢使用 Faker.js 库。
构建干净的组件
为了管理我们可能需要的所有加载和错误状态,我喜欢使用 React Query 库。您可以阅读文档,了解如何将其添加到项目中的所有细节。基本上,您只需添加一个自定义提供程序来包装您的应用,因为 React Query 还管理请求的缓存。
import { useState } from 'react';
import { useQuery } from 'react-query';
import { ListPostsService } from '../../services/definitions/list-posts-service';
import Pagination from '../Pagination/Pagination';
import PostCard from '../PostCard/PostCard';
type CleanBlogPostsProps = {
listPostsService: ListPostsService;
};
const CleanBlogPosts: React.FC<CleanBlogPostsProps> = ({ listPostsService }) => {
const [page, setPage] = useState<number>(1);
const {
data: posts,
isLoading,
isError,
} = useQuery(['posts', page], () => listPostsService.list({ page }), { initialData: [] });
if (isLoading) {
return <p data-testid="loading-posts">Loading posts...</p>;
}
if (isError) {
return <p data-testid="loading-posts-error">There was an error trying to load the posts.</p>;
}
return (
<div>
{posts!.map((post) => (
<PostCard key={post.id} post={post} />
))}
<Pagination page={page} onChangePage={setPage} />
</div>
);
};
export default CleanBlogPosts;
现在你看清楚了吗?因为useQuery
我们拥有了所有需要的状态:数据、加载和错误状态。你不再需要使用 了useEffect
。 中的第一个参数useQuery
可以是字符串或数组。当我将 包含page
在这个数组中时,这意味着查询将使用这个新值重新获取(每当页面发生变化时,例如 中的情况useEffect
)。
我还添加了一些data-testid
用于测试的内容。那么,开始构建吧!
构建组件测试
我们的组件需要listPostsService
,所以让ListPostsServiceSpy
我们使用之前创建的 。使用它我们不会发出真正的 HTTP 请求,因为它是一个“伪服务”。
import { render, screen } from '@testing-library/react';
import reactQuery, { UseQueryResult } from 'react-query';
import { ListPostsServiceSpy } from '../../services/definitions/mock-list-posts-service';
import CleanBlogPosts from './CleanBlogPosts';
type SutTypes = {
listPostsServiceSpy: ListPostsServiceSpy;
};
const makeSut = (): SutTypes => {
const listPostsServiceSpy = new ListPostsServiceSpy();
return {
listPostsServiceSpy,
};
};
jest.mock('react-query', () => ({
useQuery: () => {
return {
data: [],
isLoading: false,
isError: false,
};
},
}));
describe('CleanBlogPosts', () => {
it('should show loading state', async () => {
const { listPostsServiceSpy } = makeSut();
jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
data: listPostsServiceSpy.result,
isLoading: true,
isError: false,
} as any);
render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);
expect(screen.getByTestId('loading-posts')).toBeInTheDocument();
});
it('should show error state', async () => {
const { listPostsServiceSpy } = makeSut();
jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
data: listPostsServiceSpy.result,
isLoading: false,
isError: true,
} as any);
render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);
expect(screen.getByTestId('loading-posts-error')).toBeInTheDocument();
});
it('should list the posts', async () => {
const { listPostsServiceSpy } = makeSut();
jest.spyOn(reactQuery, 'useQuery').mockReturnValueOnce({
data: listPostsServiceSpy.result,
isLoading: false,
isError: false,
} as UseQueryResult);
render(<CleanBlogPosts listPostsService={listPostsServiceSpy} />);
const posts = await screen.findAllByTestId('post-card');
expect(posts).toHaveLength(listPostsServiceSpy.result.length);
});
});
我们添加了 3 个测试:
- 加载状态:检查是否
useQuery
返回状态isLoading: true
,我们将渲染加载组件。 - 错误状态:检查是否
useQuery
返回状态isError: true
,我们将渲染错误组件。 - 成功:检查我们的
useQuery
状态是否返回data
,如果返回,我们将渲染所需的内容(帖子列表卡片)。我还检查了我们渲染的帖子数量是否与服务返回的帖子数量相同。
结论
这并不是“最适合你的 API 的解决方案”。每种情况可能需要不同的解决方案。但我希望这能帮助你找到开发更好代码的替代方案。
另一种选择是创建一个名为的自定义钩子useListPosts()
,它将返回与相同的状态useQuery
,但您也可以将 React Query 与组件分离并使用您自己的实现来创建更多测试。
遗憾的是,自动化测试在前端代码中并不常见,课程中也很少讲授。现在就打开你的 VSCode 试试吧 :)
文章来源:https://dev.to/eliasjnior/how-to-make-asynchronous-requests-to-your-api-in-react-1a7m