Chốt trước một nguyên tắc: URL là nguồn sự thật cho filter/sort/page, còn dữ liệu là cache theo bộ tham số đó.
Vì sao đặt ở URL thay vì useState: người dùng chia sẻ được link đúng kết quả đang xem, F5 không mất bộ lọc, nút Back lùi về bộ lọc trước.
js
const [params, setParams] = useSearchParams()
const query = {
page: Number(params.get('page') ?? 1),
sort: params.get('sort') ?? '-created_at',
status: params.get('status') ?? '',
q: params.get('q') ?? '',
}
const { data, isPlaceholderData } = useQuery({
queryKey: ['orders', query],
queryFn: ({ signal }) => fetchOrders(query, signal),
placeholderData: keepPreviousData,
})Các quyết định quan trọng:
- Đổi filter phải reset
pagevề 1, nếu không người dùng đang ở trang 7 lọc lại sẽ thấy bảng rỗng. - Giữ dữ liệu trang trước khi tải trang mới (
keepPreviousData) để bảng không nháy trắng; hiển thị trạng thái mờ + vô hiệu hoá nút chuyển trang thay vì tháo cả bảng. - Debounce ô tìm kiếm (~300ms) nhưng không debounce thao tác sort/đổi trang.
- Phân trang theo cursor thay vì
OFFSETkhi bảng rất lớn:OFFSET 100000buộc DB duyệt qua toàn bộ số dòng bị bỏ, và dữ liệu chèn mới giữa các lần gọi làm lệch trang. Đổi lại, cursor thì khó nhảy tới trang bất kỳ. - Server trả kèm
totalđể dựng thanh phân trang; nếu đếm quá tốn kém thì trảhasNextvà dùng dạng "Trước / Sau". - Sort và filter phải nằm trong danh sách cột được phép ở server — ghép thẳng chuỗi từ query vào SQL là lỗ hổng injection.