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​