Chuyển tới nội dung chính

Common Patterns

Hướng dẫn các pattern thường dùng khi tích hợp Cohost API.

Pagination

Request Parameters

ParameterTypeDescription
pageintegerPage number (bắt đầu từ 1)
limitintegerSố 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

ParameterTypeDescription
statusstringFilter theo status (active, inactive, etc.)
searchstringSearch theo tên
locationstringFilter theo location
start_datedateFilter từ ngày
end_datedateFilter đế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

ParameterTypeDescription
sort_bystringField to sort by
sort_orderstringasc 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} />;
}
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