Common Patterns
Hướng dẫn các pattern thường dùng khi tích hợp Cohost API.
Pagination
Request Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (bắt đầu từ 1) |
limit | integer | Số items per page (default: 20) |
Response Structure
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
total_pages: number;
}
Ví dụ sử dụng
import { useState } from 'react';
import { useGetListingsApiV1ListingsGet } from '@/generated/api/listings/listings';
function PaginatedListings() {
const [page, setPage] = useState(1);
const { data, isLoading } = useGetListingsApiV1ListingsGet({
page,
limit: 20,
});
const totalPages = data?.total_pages || 1;
return (
<div>
{isLoading ? (
<Loading />
) : (
<>
<ul>
{data?.items?.map((listing) => (
<ListingItem key={listing.id} listing={listing} />
))}
</ul>
<div className="pagination">
<button
disabled={page === 1}
onClick={() => setPage(p => p - 1)}
>
Previous
</button>
<span>
Page {page} of {totalPages}
</span>
<button
disabled={page === totalPages}
onClick={() => setPage(p => p + 1)}
>
Next
</button>
</div>
</>
)}
</div>
);
}
Filtering
Common Filter Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter theo status (active, inactive, etc.) |
search | string | Search theo tên |
location | string | Filter theo location |
start_date | date | Filter từ ngày |
end_date | date | Filter đến ngày |
Ví dụ
import { useGetListingsApiV1ListingsGet } from '@/generated/api/listings/listings';
function FilteredListings({ searchQuery, statusFilter }: Props) {
const { data } = useGetListingsApiV1ListingsGet({
search: searchQuery,
status: statusFilter,
page: 1,
limit: 50,
});
return (
<ul>
{data?.items?.map((listing) => (
<li key={listing.id}>{listing.name}</li>
))}
</ul>
);
}
Sorting
Sort Parameters
| Parameter | Type | Description |
|---|---|---|
sort_by | string | Field to sort by |
sort_order | string | asc hoặc desc |
const { data } = useGetListingsApiV1ListingsGet({
sort_by: 'created_at',
sort_order: 'desc',
});
Error Handling
Error Response Structure
interface ApiError {
error: {
code: string;
message: string;
details?: Record<string, string[]>;
};
}
Xử lý Error trong React Query
import { useGetListingsApiV1ListingsGet } from '@/generated/api/listings/listings';
function ListingPage() {
const { data, error, isError } = useGetListingsApiV1ListingsGet({});
if (isError) {
return (
<ErrorDisplay
code={error?.error?.code}
message={error?.error?.message}
/>
);
}
return <div>{/* ... */}</div>;
}
Global Error Handler
// lib/error-handler.ts
import { AxiosError } from 'axios';
export interface ApiErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, string[]>;
};
}
export function handleApiError(error: unknown): string {
if (error instanceof AxiosError<ApiErrorResponse>) {
const data = error.response?.data;
if (data?.error?.message) {
return data.error.message;
}
if (error.response?.status === 401) {
return 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.';
}
if (error.response?.status === 403) {
return 'Bạn không có quyền thực hiện thao tác này.';
}
if (error.response?.status === 429) {
return 'Quá nhiều yêu cầu. Vui lòng thử lại sau.';
}
}
return 'Đã xảy ra lỗi. Vui lòng thử lại.';
}
Optimistic Updates
Cập nhật UI ngay lập tức
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateListingApiV1ListingsListingIdPatch } from '@/generated/api/listings/listings';
function UpdateListingForm({ listing }: { listing: Listing }) {
const queryClient = useQueryClient();
const updateListing = useUpdateListingApiV1ListingsListingIdPatch();
const handleUpdate = async (updates: Partial<Listing>) => {
await updateListing.mutateAsync(
{
listingId: listing.id,
data: updates,
},
{
// Optimistic update
onMutate: async (newData) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['listings'] });
// Snapshot previous value
const previousListings = queryClient.getQueryData(['listings']);
// Optimistically update
queryClient.setQueryData(['listings'], (old: any) =>
old?.map((item: Listing) =>
item.id === listing.id ? { ...item, ...newData } : item
)
);
return { previousListings };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['listings'], context?.previousListings);
},
}
);
};
return <Form onSubmit={handleUpdate} />;
}
Debounced Search
import { useState, useEffect } from 'react';
import { useGetListingsApiV1ListingsGet } from '@/generated/api/listings/listings';
function SearchListings() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// Debounce search
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 300);
return () => clearTimeout(timer);
}, [query]);
const { data } = useGetListingsApiV1ListingsGet(
{ search: debouncedQuery },
{ enabled: debouncedQuery.length > 0 }
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search listings..."
/>
<Results items={data?.items} />
</div>
);
}
Loading States
Skeleton Loading
function ListingSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-image" />
<div className="skeleton-title" />
<div className="skeleton-text" />
</div>
);
}
function ListingPage() {
const { data, isLoading } = useGetListingsApiV1ListingsGet({});
if (isLoading) {
return (
<div>
<ListingSkeleton />
<ListingSkeleton />
<ListingSkeleton />
</div>
);
}
return <ListingsList items={data?.items} />;
}
Caching
Invalidate Cache
import { useQueryClient } from '@tanstack/react-query';
function RefreshButton() {
const queryClient = useQueryClient();
const handleRefresh = () => {
// Invalidate và refetch
queryClient.invalidateQueries({ queryKey: ['listings'] });
};
return <button onClick={handleRefresh}>Refresh</button>;
}
Set Cache Manually
// Update cache without API call
queryClient.setQueryData(['listing', listingId], newListingData);
Tiếp theo
- Code Examples: Ví dụ code chi tiết