ข้ามไปยังเนื้อหาหลัก

การใช้ร่วมกับ React Query

ปัญหาเรื่อง "จะวาง Keys ไว้ที่ไหน"​

วิธีแก้ — แบ่งตาม Entities​

ถ้าโปรเจกต์มีการแบ่งเป็น Entities อยู่แล้ว และแต่ละ Request ตรงกับ Entity เดียว การแบ่งที่สะอาดที่สุดคือแบ่งตาม Entity ในกรณีนี้ เราแนะนำให้ใช้โครงสร้างดังนี้:

└── src/                                        #
├── app/ #
| ... #
├── pages/ #
| ... #
├── entities/ #
| ├── {entity}/ #
| ... └── api/ #
| ├── `{entity}.query` # Query-factory ที่เก็บ Keys และ Functions
| ├── `get-{entity}` # Entity getter function
| ├── `create-{entity}` # Entity creation function
| ├── `update-{entity}` # Entity update function
| ├── `delete-{entity}` # Entity delete function
| ... #
| #
├── features/ #
| ... #
├── widgets/ #
| ... #
└── shared/ #
... #

ถ้ามีการเชื่อมต่อกันระหว่าง Entities (เช่น Entity Country มี field-list ของ City entities), คุณสามารถใช้ Public API for cross-imports หรือพิจารณาทางเลือกอื่นข้างล่าง

ทางเลือกอื่น — เก็บไว้ใน Shared​

ในกรณีที่การแยก Entity ไม่เหมาะสม สามารถพิจารณาโครงสร้างต่อไปนี้:

└── src/                                        #
... #
└── shared/ #
├── api/ #
... ├── `queries` # Query-factories
| ├── `document.ts` #
| ├── `background-jobs.ts` #
| ... #
└── index.ts #

จากนั้นใน @/shared/api/index.ts:

@/shared/api/index.ts
export { documentQueries } from "./queries/document";

ปัญหาเรื่อง "จะแทรก Mutations ตรงไหน?"​

ไม่แนะนำให้ผสม Mutations กับ Queries มีสองทางเลือก:

1. Define custom hook ใน api segment ใกล้ๆ กับที่ใช้งาน​

@/features/update-post/api/use-update-title.ts
export const useUpdateTitle = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ id, newTitle }) =>
apiClient
.patch(`/posts/${id}`, { title: newTitle })
.then((data) => console.log(data)),

onSuccess: (newPost) => {
queryClient.setQueryData(postsQueries.ids(id), newPost);
},
});
};

2. Define mutation function ที่อื่น (Shared หรือ Entities) และใช้ useMutation โดยตรงใน Component​

const { mutateAsync, isPending } = useMutation({
mutationFn: postApi.createPost,
});
@/pages/post-create/ui/post-create-page.tsx
export const CreatePost = () => {
const { classes } = useStyles();
const [title, setTitle] = useState("");

const { mutate, isPending } = useMutation({
mutationFn: postApi.createPost,
});

const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate({ title, userId: DEFAULT_USER_ID });
};

return (
<form className={classes.create_form} onSubmit={handleSubmit}>
<TextField onChange={handleChange} value={title} />
<LoadingButton type="submit" variant="contained" loading={isPending}>
Create
</LoadingButton>
</form>
);
};

การจัดการ Requests​

Query Factory​

Query factory คือ Object ที่ค่าของ Key คือฟังก์ชันที่ Return รายการของ Query keys นี่คือวิธีใช้:

const keyFactory = {
all: () => ["entity"],
lists: () => [...postQueries.all(), "list"],
};
info

queryOptions เป็น Built-in utility ใน react-query@v5 (Optional)

queryOptions({
queryKey,
...options,
});

เพื่อความ Type safety ที่มากขึ้น, การเข้ากันได้กับ React-query เวอร์ชันในอนาคต, และการเข้าถึง Functions และ Query keys ที่ง่ายขึ้น คุณสามารถใช้ฟังก์ชัน queryOptions ที่มากับ “@tanstack/react-query” ได้ (รายละเอียดเพิ่มเติมที่นี่)

1. การสร้าง Query Factory​

@/entities/post/api/post.queries.ts
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { getPosts } from "./get-posts";
import { getDetailPost } from "./get-detail-post";
import { PostDetailQuery } from "./query/post.query";

export const postQueries = {
all: () => ["posts"],

lists: () => [...postQueries.all(), "list"],
list: (page: number, limit: number) =>
queryOptions({
queryKey: [...postQueries.lists(), page, limit],
queryFn: () => getPosts(page, limit),
placeholderData: keepPreviousData,
}),

details: () => [...postQueries.all(), "detail"],
detail: (query?: PostDetailQuery) =>
queryOptions({
queryKey: [...postQueries.details(), query?.id],
queryFn: () => getDetailPost({ id: query?.id }),
staleTime: 5000,
}),
};

2. การใช้ Query Factory ใน Application Code​

import { useParams } from "react-router-dom";
import { postApi } from "@/entities/post";
import { useQuery } from "@tanstack/react-query";

type Params = {
postId: string;
};

export const PostPage = () => {
const { postId } = useParams<Params>();
const id = parseInt(postId || "");
const {
data: post,
error,
isLoading,
isError,
} = useQuery(postApi.postQueries.detail({ id }));

if (isLoading) {
return <div>Loading...</div>;
}

if (isError || !post) {
return <>{error?.message}</>;
}

return (
<div>
<p>Post id: {post.id}</p>
<div>
<h1>{post.title}</h1>
<div>
<p>{post.body}</p>
</div>
</div>
<div>Owner: {post.userId}</div>
</div>
);
};

ประโยชน์ของการใช้ Query Factory​

  • จัดระเบียบ Request: Factory ช่วยให้คุณจัดการ API requests ทั้งหมดไว้ที่เดียว ทำให้โค้ดอ่านง่ายและดูแลรักษาง่าย
  • เข้าถึง Queries และ Keys ได้สะดวก: Factory มี Methods ที่สะดวกสำหรับเข้าถึง Queries ประเภทต่างๆ และ Keys ของมัน
  • ความสามารถในการ Refetch Query: Factory ช่วยให้ Refetch ได้ง่ายโดยไม่ต้องเปลี่ยน Query keys ในส่วนต่างๆ ของ Application

Pagination​

ในส่วนนี้ เราจะดูตัวอย่างฟังก์ชัน getPosts ซึ่งทำการ API Request เพื่อดึง Post entities โดยใช้ Pagination

1. สร้างฟังก์ชัน getPosts​

ฟังก์ชัน getPosts อยู่ในไฟล์ get-posts.ts ซึ่งอยู่ใน api segment

@/pages/post-feed/api/get-posts.ts
import { apiClient } from "@/shared/api/base";

import { PostWithPaginationDto } from "./dto/post-with-pagination.dto";
import { PostQuery } from "./query/post.query";
import { mapPost } from "./mapper/map-post";
import { PostWithPagination } from "../model/post-with-pagination";

const calculatePostPage = (totalCount: number, limit: number) =>
Math.floor(totalCount / limit);

export const getPosts = async (
page: number,
limit: number,
): Promise<PostWithPagination> => {
const skip = page * limit;
const query: PostQuery = { skip, limit };
const result = await apiClient.get<PostWithPaginationDto>("/posts", query);

return {
posts: result.posts.map((post) => mapPost(post)),
limit: result.limit,
skip: result.skip,
total: result.total,
totalPages: calculatePostPage(result.total, limit),
};
};

2. Query factory สำหรับ Pagination​

postQueries query factory กำหนด Query options ต่างๆ สำหรับทำงานกับ Posts รวมถึงการ Request รายการ Posts แบบระบุ Page และ Limit

import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { getPosts } from "./get-posts";

export const postQueries = {
all: () => ["posts"],
lists: () => [...postQueries.all(), "list"],
list: (page: number, limit: number) =>
queryOptions({
queryKey: [...postQueries.lists(), page, limit],
queryFn: () => getPosts(page, limit),
placeholderData: keepPreviousData,
}),
};

3. ใช้ใน Application Code​

@/pages/home/ui/index.tsx
export const HomePage = () => {
const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN;
const [page, setPage] = usePageParam(DEFAULT_PAGE);
const { data, isFetching, isLoading } = useQuery(
postApi.postQueries.list(page, itemsOnScreen),
);
return (
<>
<Pagination
onChange={(_, page) => setPage(page)}
page={page}
count={data?.totalPages}
variant="outlined"
color="primary"
/>
<Posts posts={data?.posts} />
</>
);
};
note

ตัวอย่างถูกทำให้ง่ายขึ้น เวอร์ชันเต็มดูได้ที่ GitHub

QueryProvider สำหรับจัดการ Queries​

ในไกด์นี้ เราจะดูวิธีการจัดระเบียบ QueryProvider

1. สร้าง QueryProvider​

ไฟล์ query-provider.tsx อยู่ที่ path @/app/providers/query-provider.tsx

@/app/providers/query-provider.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactNode } from "react";

type Props = {
children: ReactNode;
client: QueryClient;
};

export const QueryProvider = ({ client, children }: Props) => {
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
);
};

2. สร้าง QueryClient​

QueryClient คือ Instance ที่ใช้จัดการ API requests ไฟล์ query-client.ts อยู่ที่ @/shared/api/query-client.ts QueryClient ถูกสร้างด้วย Settings บางอย่างสำหรับ Query caching

@/shared/api/query-client.ts
import { QueryClient } from "@tanstack/react-query";

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 5 * 60 * 1000,
},
},
});

Code generation​

มีเครื่องมือที่สามารถ Generate API code ให้คุณได้ แต่มันยืดหยุ่นน้อยกว่าวิธี Manual ที่อธิบายข้างต้น ถ้า Swagger file ของคุณมีโครงสร้างที่ดี และคุณใช้หนึ่งในเครื่องมือเหล่านี้ มันอาจสมเหตุสมผลที่จะ Generate โค้ดทั้งหมดในไดเรกทอรี @/shared/api

คำแนะนำเพิ่มเติมสำหรับจัดระเบียบ RQ​

API Client​

การใช้ Custom API client class ใน Shared layer, คุณสามารถทำให้ Configuration และการทำงานกับ API ในโปรเจกต์เป็นมาตรฐานเดียวกันได้ สิ่งนี้ช่วนให้คุณจัดการ Logging, Headers และ Data exchange format (เช่น JSON หรือ XML) ได้จากที่เดียว วิธีนี้ทำให้ดูแลรักษาและพัฒนาโปรเจกต์ง่ายขึ้นเพราะมันทำให้การเปลี่ยนและอัปเดตการปฏิสัมพันธ์กับ API ง่ายขึ้น

@/shared/api/api-client.ts
import { API_URL } from "@/shared/config";

export class ApiClient {
private baseUrl: string;

constructor(url: string) {
this.baseUrl = url;
}

async handleResponse<TResult>(response: Response): Promise<TResult> {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

try {
return await response.json();
} catch (error) {
throw new Error("Error parsing JSON response");
}
}

public async get<TResult = unknown>(
endpoint: string,
queryParams?: Record<string, string | number>,
): Promise<TResult> {
const url = new URL(endpoint, this.baseUrl);

if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value.toString());
});
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});

return this.handleResponse<TResult>(response);
}

public async post<TResult = unknown, TData = Record<string, unknown>>(
endpoint: string,
body: TData,
): Promise<TResult> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

return this.handleResponse<TResult>(response);
}
}

export const apiClient = new ApiClient(API_URL);

ดูเพิ่มเติม​