- vừa được xem lúc

Công cụ React Query: Các hook tái sử dụng, an toàn kiểu dữ liệu với Axios và FSD.

0 0 3

Người đăng: Tiến Minh

Theo Viblo Asia

Trong thế giới phát triển frontend hiện đại, tối ưu hóa việc xử lý dữ liệu là chìa khóa để tạo ra các ứng dụng phản hồi nhanh và có thể mở rộng. Việc sử dụng các thư viện cho các yêu cầu bất đồng bộ, như @tanstack/react-query và axios, cung cấp một hệ thống tiện lợi để làm việc với REST API, tRPC hoặc GraphQL.

Khi phát triển ứng dụng, chúng ta thường cần thực hiện các hành động giống nhau cho các thực thể API khác nhau: lấy danh sách các mục, truy xuất một mục đơn theo ID, tạo một mục mới, cũng như thực hiện các cập nhật và xóa bỏ. Những thao tác này được gọi là CRUD (Tạo, Đọc, Cập nhật, Xóa).

Mình sẽ chỉ cách triển khai các hook CRUD có thể tái sử dụng bằng các thư viện trên theo phong cách hàm, dựa trên mẫu thiết kế factory. Thay vì sao chép mã cho mỗi thực thể, mình có thể tạo ra các hook, điều này giúp giảm đáng kể lượng mã sao chép và làm cho mã linh hoạt và tái sử dụng được. Phương pháp này dễ dàng mở rộng, có tính ứng dụng cao và có thể áp dụng cho bất kỳ API nào.

@tanstack/react-query and axios

  • @tanstack/react-query — trước đây được gọi là react-query, là một thư viện dùng để quản lý trạng thái của các yêu cầu, lưu trữ dữ liệu vào bộ nhớ đệm và đồng bộ hóa với máy chủ. Nó cải thiện hiệu suất một cách đáng kể và đơn giản hóa việc làm việc với các yêu cầu bất đồng bộ bằng cách trừu tượng hóa logic API khỏi logic render dữ liệu trong các component React.
  • axios là một thư viện phổ biến dùng để thực hiện các yêu cầu HTTP. Nó được biết đến với tính linh hoạt và dễ sử dụng, đặc biệt khi làm việc với các thao tác bất đồng bộ.

Mục tiêu của chúng ta là tạo ra một hàm tự động sinh ra các CRUD hooks cho bất kỳ entity nào, giúp tiết kiệm việc phải viết lại cùng một mã lặp đi lặp lại. Ví dụ, nếu chúng ta cần lấy danh sách người dùng, bài viết hay bình luận, chúng ta không muốn phải lặp lại cùng một đoạn mã boilerplate cho mỗi entity.

Triển khai CRUD Hooks theo kiểu hàm

Hãy bắt đầu với việc triển khai. Chúng ta sẽ tạo một hàm createCrudHooks trả về một bộ CRUD hooks cho bất kỳ entity nào.

Bước 1: Kết nối các thư viện cần thiết

Để xử lý các truy vấn và lưu trữ dữ liệu, cài đặt các thư viện @tanstack/react-query và axios (sau khi chuyển vào thư mục dự án của bạn):

cd ./my-awesome-project
npm i axios react-query

Bước 2: Tạo hàm chính

Bây giờ, tạo hàm createCrudHooks, hàm này nhận một URL cơ sở và tên entity (ví dụ: users hoặc posts). Hàm sẽ trả về một bộ hooks để thực hiện các thao tác CRUD.

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import axios, { AxiosResponse } from 'axios' const createCrudHooks = <T>(baseUrl: string, entity: string) => { const api = axios.create({ baseURL: baseUrl, }) // Fetch All (GET) const useFetchAll = (queryKey: string) => { return useQuery<T[]>(queryKey, async () => { const response: AxiosResponse<T[]> = await api.get(`/${entity}`) return response.data }) } // Fetch One by ID (GET) const useFetchOne = (queryKey: string, id: string | number) => { return useQuery<T>([queryKey, id], async () => { const response: AxiosResponse<T> = await api.get(`/${entity}/${id}`) return response.data }) } // Create (POST) const useCreate = () => { const queryClient = useQueryClient() return useMutation( async (data: Partial<T>) => { const response: AxiosResponse<T> = await api.post(`/${entity}`, data) return response.data }, { onSuccess: () => { queryClient.invalidateQueries(entity) }, } ) } // Update (PUT) const useUpdate = () => { const queryClient = useQueryClient() return useMutation( async ({ id, data }: { id: string | number; data: Partial<T> }) => { const response: AxiosResponse<T> = await api.put( `/${entity}/${id}`, data ) return response.data }, { onSuccess: () => { queryClient.invalidateQueries(entity) }, } ) } // Delete (DELETE) const useDelete = () => { const queryClient = useQueryClient() return useMutation( async (id: string | number) => { const response: AxiosResponse<void> = await api.delete( `/${entity}/${id}` ) return response.data }, { onSuccess: () => { queryClient.invalidateQueries(entity) }, } ) } return { useFetchAll, useFetchOne, useCreate, useUpdate, useDelete }
}

Bạn có thể tìm hiểu thêm về cách sử dụng type-safe với @tanstack/react-query trong bài viết Type-safe React Query.

Hãy cùng xem xét chi tiết các hooks mà hàm createCrudHooks trả về:

Hook useFetchAll
Hook này gửi một yêu cầu GET để lấy danh sách các mục cho một entity cụ thể (ví dụ, danh sách người dùng) và sử dụng hook useQuery từ thư viện @tanstack/react-query để lưu trữ và theo dõi trạng thái của yêu cầu. Nó trả về một mảng các mục mà có thể được sử dụng trong bất kỳ component React nào để hiển thị hoặc thao tác thêm. Hàm này có thể được mở rộng để truyền các tham số truy vấn bổ sung, ví dụ như lọc kết quả.

Hook useFetchOne
Hook này lấy một mục đơn theo ID (mã định danh duy nhất). Nó cũng sử dụng useQuery, nhưng nhận một tham số thứ hai là ID của mục. Điều này cho phép bạn lấy dữ liệu cho một mục cụ thể trong entity.

Hook useCreate
Hook này được sử dụng để tạo các mục mới. Nó gửi một yêu cầu POST đến API và, sau khi tạo thành công một mục, gọi invalidateQueries để làm mới bộ nhớ cache, tự động tải lại dữ liệu trong các component khác nơi các hooks của chúng ta đang được sử dụng.

Hook useUpdate
Một yêu cầu PUT được sử dụng để cập nhật một mục. Hook này nhận một đối tượng chứa ID của mục và dữ liệu cần cập nhật. Sau khi cập nhật thành công, bộ nhớ cache của dữ liệu cũng sẽ được làm mới.

Hook useDelete
Hook này xóa một mục theo ID bằng yêu cầu DELETE và làm mới bộ nhớ cache sau khi xóa.

Bước 3: Viết test

Hãy thêm các bài test để đảm bảo rằng đoạn mã của chúng ta hoạt động chính xác và không xảy ra lỗi.

Trước tiên, hãy thiết lập môi trường test với các thư viện sau:

  • @testing-library/react — dùng để test các component React.
  • @testing-library/jest-dom — cung cấp thêm các matcher để kiểm tra (ví dụ: toBeInTheDocument).
  • vitest — một công cụ chạy và viết test (test runner và framework).
  • axios-mock-adapter — dùng để giả lập (mock) các yêu cầu HTTP qua axios.

npm i @testing-library/react @testing-library/jest-dom vitest axios-mock-adapter -D

Để chạy test, hãy thêm đoạn script sau vào file package.json:

"scripts": { "test": "vitest"
}

Sau đó chạy lệnh
npm run test

Hoặc nếu bạn dùng yarn
npm run test

Hãy tạo một file kiểm thử, ở đây tôi sẽ tạo file: crudHooks.test.tsx:

import { QueryClient, QueryClientProvider } from 'react-query'
import { act, renderHook } from '@testing-library/react'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
// Your hooks
import { createCrudHooks } from './crudHooks' const mock = new MockAdapter(axios) const createTestQueryClient = () => { return new QueryClient({ defaultOptions: { queries: { retry: false, }, }, })
} const wrapper = ({ children }: { children: React.ReactNode }) => { const queryClient = createTestQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> )
} interface User { id: number name: string email: string
} // Create hooks for user entity
const { useFetchAll, useFetchOne, useCreate, useUpdate, useDelete } = createCrudHooks<User>('https://api.example.io', 'users') beforeEach(() => { mock.reset()
}) describe('CRUD Hooks', () => { it('should fetch all users successfully', async () => { const mockData = [ { id: 1, name: 'Ivan Green', email: 'ivan@example.io' }, { id: 2, name: 'Inna Green', email: 'inna@example.io' }, ] mock.onGet('/users').reply(200, mockData) const { result, waitFor } = renderHook(() => useFetchAll('users'), { wrapper, }) await waitFor(() => result.current.isSuccess) expect(result.current.data).toEqual(mockData) }) it('should fetch one user by ID', async () => { const mockUser = { id: 1, name: 'Ivan Green', email: 'ivan@example.io' } mock.onGet('/users/1').reply(200, mockUser) const { result, waitFor } = renderHook(() => useFetchOne('users', 1), { wrapper, }) await waitFor(() => result.current.isSuccess) expect(result.current.data).toEqual(mockUser) }) it('should create a new user', async () => { const newUser = { id: 3, name: 'Sam Smith', email: 'sam@example.io' } mock.onPost('/users').reply(201, newUser) const { result, waitFor } = renderHook(() => useCreate(), { wrapper }) act(() => { result.current.mutate({ name: 'Sam Smith', email: 'sam@example.io' }) }) await waitFor(() => result.current.isSuccess) expect(result.current.data).toEqual(newUser) }) it('should update a user', async () => { const updatedUser = { id: 1, name: 'Ivan Updated', email: 'ivan.updated@example.io', } mock.onPut('/users/1').reply(200, updatedUser) const { result, waitFor } = renderHook(() => useUpdate(), { wrapper }) act(() => { result.current.mutate({ id: 1, data: { name: 'Ivan Updated', email: 'Ivan.updated@example.io' }, }) }) await waitFor(() => result.current.isSuccess) expect(result.current.data).toEqual(updatedUser) }) it('should delete a user', async () => { mock.onDelete('/users/1').reply(200) const { result, waitFor } = renderHook(() => useDelete(), { wrapper }) act(() => { result.current.mutate(1) }) await waitFor(() => result.current.isSuccess) expect(result.current.isSuccess).toBe(true) })
})

Chúng ta đã tạo các bài kiểm thử cho bộ hook CRUD bằng cách sử dụng react-testing-library, axios-mock-adapter, và vitest. Điều này giúp cho mã của mình trở nên đáng tin cậy và được kiểm thử kỹ lưỡng — một yếu tố quan trọng trong việc phát triển các ứng dụng có thể mở rộng và dễ bảo trì. Việc kiểm thử các thao tác bất đồng bộ cũng trở nên dễ dàng hơn nhờ vào việc mô phỏng request và các công cụ hỗ trợ làm việc với hook.

Ví dụ sử dụng trong một component React

Giờ đây, khi đã có các hook dùng chung, hãy xem cách sử dụng chúng trong một component thực tế:

interface User { id: number name: string email: string
} // Can be moved to a separate file for reuse
const { useFetchAll, useCreate, useUpdate, useDelete } = createCrudHooks<User>( 'https://api.example.io', 'users'
) type UserProps = { user: User
} const UserItem = ({ user }: UserProps) => { const { mutate: updateUser } = useUpdate() const { mutate: deleteUser } = useDelete() const handleUpdate = () => { updateUser({ id: user.id, data: { name: 'Ivan' } }) } const handleDelete = () => { deleteUser(user.id) } return ( <li key={user.id}> {user.name} - {user.email} <button onClick={handleUpdate}>Update name</button> <button onClick={handleDelete}>Delete</button> </li> )
} export const Users = () => { const { data: users, isLoading } = useFetchAll('users') const { mutate: createUser } = useCreate() const handleCreateUser = () => { createUser({ name: 'Darya', email: 'darya@example.io' }) } if (isLoading) return <div>Loading users...</div> return ( <div> <h1>Users</h1> <ul> {users?.map((user: User) => ( <UserItem key={user.id} user={user} /> ))} </ul> <button onClick={handleCreateUser}>Create user</button> </div> )
}

Query Factory và FSD

Giờ hãy cùng xem cách tổ chức kiến trúc dự án sử dụng query factory kết hợp với phương pháp Feature-Sliced Design (FSD).

Cấu trúc dự án

src/
├── app/ // Global configurations, routing, and providers
├── entities/ // Entities
│ └── pokemon/
│ ├── api/
│ ├── model/
│ └── ui/
├── features/ // Features
├── widgets/ // Widgets
├── pages/ // Pages (PokemonsPage for example)
└── shared/ // Common modules, such as UI components, utilities..

Shared (Tài nguyên dùng chung)

Shared bao gồm các component dùng chung, utility, cấu hình, và trình trợ giúp API tổng quát.

Hãy viết một hàm createQueries, đây sẽ là query factory – một nơi tạo ra các truy vấn dùng chung; ta giả định đang làm việc với một API đơn giản hỗ trợ các thao tác CRUD để giao tiếp với server:

import { keepPreviousData, queryOptions } from '@tanstack/react-query';
import * as qs from 'qs';
import { createQueryFn } from '../../api/createQueryFn';
import { createMutationFn } from '../../api/createMutationFn';
import { createDeleteMutationFn } from '../../api/createDeleteMutationFn';
import { createUpdateMutationFn } from '../../api/createUpdateMutationFn'; // We pass generics so that when we write code later, the types are displayed correctly.
export const createQueries = < CreateResponse, CreateBody, ReadResponse, ReadOneResponse, UpdateResponse, UpdateBody, DeleteResponse, DeleteParams
>( entity: string
) => ({ all: () => queryOptions({ queryKey: [entity], }), create: () => ({ mutationKey: [entity], mutationFn: (body: CreateBody) => createMutationFn<CreateResponse, CreateBody>({ path: `/${entity}`, body, }), placeholderData: keepPreviousData, }), read: (filters) => queryOptions({ queryKey: [entity, filters], queryFn: () => createQueryFn<ReadResponse>({ path: `/${entity}?${qs.stringify(filters)}`, }), placeholderData: keepPreviousData, }), readOne: ({ id }) => queryOptions({ queryKey: [entity, id], queryFn: () => createQueryFn<ReadOneResponse>({ path: `/${entity}/${id}`, }), placeholderData: keepPreviousData, }), update: () => ({ mutationKey: [entity], mutationFn: ({ id, body }) => createUpdateMutationFn<UpdateResponse, UpdateBody>({ path: `/${entity}/${id}`, body, }), placeholderData: keepPreviousData, }), delete: () => ({ mutationKey: [entity], mutationFn: (params: DeleteParams) => createDeleteMutationFn<DeleteResponse>({ path: `/${entity}/${params.id}`, }), placeholderData: keepPreviousData, }),
});

Hàm này trả về một object chứa các cấu hình truy vấn (query configurations) cho các thao tác CRUD cơ bản.

Hãy tạo một file test có tên là createQueries.test.ts:

import { describe, it, expect, vi } from 'vitest';
import { createQueries } from './createQueries.ts';
import * as api from '../../api/createQueryFn';
import * as mutationApi from '../../api/createMutationFn'; vi.mock('../../api/createQueryFn', () => ({ createQueryFn: vi.fn(),
})); vi.mock('../../api/createMutationFn', () => ({ createMutationFn: vi.fn(),
})); vi.mock('../../api/createDeleteMutationFn', () => ({ createDeleteMutationFn: vi.fn(),
})); vi.mock('../../api/createUpdateMutationFn', () => ({ createUpdateMutationFn: vi.fn(),
})); describe('createQueries', () => { const entity = 'user'; it('should return correct query options for "all"', () => { const queries = createQueries(entity); const result = queries.all(); expect(result.queryKey).toEqual([entity]); }); it('should create mutation for "create"', () => { const queries = createQueries(entity); const body = { name: 'Alexander' }; queries.create().mutationFn(body); expect(mutationApi.createMutationFn).toHaveBeenCalledWith({ path: `/${entity}`, body, }); }); it('should return correct query options for "read"', () => { const queries = createQueries(entity); const filters = { page: 1 }; queries.read(filters).queryFn(); expect(api.createQueryFn).toHaveBeenCalledWith({ path: `/${entity}?page=1`, }); }); it('should return correct query options for "readOne"', () => { const queries = createQueries(entity); const id = 123; queries.readOne({ id }).queryFn(); expect(api.createQueryFn).toHaveBeenCalledWith({ path: `/${entity}/${id}`, }); }); it('should create mutation for "update"', () => { const queries = createQueries(entity); const id = 123; const body = { name: 'Updated' }; queries.update().mutationFn({ id, body }); expect(mutationApi.createUpdateMutationFn).toHaveBeenCalledWith({ path: `/${entity}/${id}`, body, }); }); it('should create mutation for "delete"', () => { const queries = createQueries(entity); const params = { id: 123 }; queries.delete().mutationFn(params); expect(mutationApi.createDeleteMutationFn).toHaveBeenCalledWith({ path: `/${entity}/${params.id}`, }); });
});

Kết luận

Chúng ta đã triển khai một hệ thống hook CRUD linh hoạt và có thể tái sử dụng, sử dụng @tanstack/react-query và axios. Giờ đây, bạn có thể dễ dàng sử dụng các hook này cho bất kỳ entity nào trong API của mình, giúp mã của bạn trở nên đơn giản hơn và dễ đọc hơn.

Cách tiếp cận này giúp mở rộng ứng dụng dễ dàng và giảm thiểu sự trùng lặp mã. Việc áp dụng nguyên tắc FSD giúp cấu trúc dự án, làm cho nó trở nên dễ mở rộng và bảo trì hơn. Kết hợp những phương pháp này giúp bạn tạo ra những ứng dụng linh hoạt, trong đó tính mô-đun và độc lập của các component làm cho dự án dễ dàng hỗ trợ và mở rộng.

Hãy thử áp dụng cách tiếp cận này vào dự án của bạn, và bạn sẽ nhanh chóng nhận thấy cách nó giúp đơn giản hóa việc quản lý trạng thái yêu cầu và tích hợp chúng vào các component React.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Những điều cần lưu ý và sử dụng Hook trong React (Phần 5)

V. Sử dụng useRef như thế nào cho đúng.

0 0 147

- vừa được xem lúc

7 Cách viết code React "clean" hơn

Mở đầu. Là nhà phát triển React, tất cả chúng ta đều muốn viết code sạch hơn, đơn giản hơn và dễ đọc hơn. 1. Sử dụng JSX shorthands.

0 0 204

- vừa được xem lúc

Create app covid-19 use Reactjs

Chào các bạn hôm nay mình sẽ chia sẻ với các bạn một app covid-19, để mọi người cùng tham khảo, tính năng của App này chỉ đơn giản là show số liệu về dịch Covid của các nước trên thế giới như: Số ngườ

0 0 59

- vừa được xem lúc

ReactJS Custom Hooks

ReactJS cung cấp rất nhiều cho bạn các hook mà bạn có thể sử dụng hằng ngày vào project của mình. Nhưng bên cạnh đó bạn có thể tự tạo ra các hook của riêng bạn để sử dụng, làm cho việc tối ưu hóa code

0 0 83

- vừa được xem lúc

3 cách để tránh re-render khi dùng React context

3 cách để tránh re-render khi dùng React context. Nếu đã từng sử dụng React context cho dự án của bạn, và gặp phải tình trạng các component con - Consumer re-render rất nhiều lần, thậm chí bị sai logi

0 0 42

- vừa được xem lúc

Tìm hiểu về React Hook: Sử dụng useDebugValue

Trong bài viết hôm này, tôi sẽ giới thiệu các bạn một React Hook tiếp theo, đó là useDebugValue. useDebugValue là gì .

0 0 61