การสร้าง Widget Dashboard ที่ปรับแต่งได้ด้วย TanStack Query
Dashboard ส่วนใหญ่สร้างในลักษณะเดิม: component ระดับบนสุดยิง API call สิบครั้ง รอทั้งหมด จากนั้น render ทุกอย่างพร้อมกัน ใน demo ดูดี ในโปรดักชัน มันสร้างปัญหาที่เพิ่มขึ้นตามเวลา endpoint เดียวที่ช้า block view ทั้งหมด error ชั่วคราวบน widget หนึ่งทำให้ screen ทั้งหมดพัง
TanStack Query (เดิมคือ React Query) แก้ปัญหานี้โดยถือว่า data fetching เป็น first-class concern แต่ละ query มี cache key, lifecycle และ error state ของตัวเอง ใน widget dashboard สิ่งนี้แปลตรง: แต่ละ widget เป็นเจ้าของข้อมูลและรับผิดชอบ loading, error และ success state ของตัวเอง
การแยก layout จากข้อมูล
ก่อนเขียน data fetching logic ใด ๆ สถาปัตยกรรมต้องการการแยกที่ชัดเจนระหว่างสอง concern: widget fetch อะไรและอยู่ที่ไหน สิ่งเหล่านี้ mutate อิสระกัน — ผู้ใช้สามารถย้าย widget โดยไม่เปลี่ยนข้อมูลที่แสดง
interface WidgetConfig {
id: string;
type: 'revenue' | 'orders' | 'users' | 'activity-feed';
title: string;
queryKey: string[];
refetchInterval?: number;
}
interface WidgetLayout {
i: string; // matches WidgetConfig.id
x: number;
y: number;
w: number;
h: number;
}
Data fetching ต่อ widget
แต่ละ widget component เรียก useQuery พร้อม configuration ของตัวเอง staleTime บอก TanStack Query ว่าจะถือว่า cached data เป็น current นานแค่ไหนก่อนที่จะ mark ว่า stale
function RevenueWidget({ config }: { config: WidgetConfig }) {
const { data, isLoading, isError } = useQuery({
queryKey: config.queryKey,
queryFn: () => fetchRevenue(),
staleTime: 60_000,
refetchInterval: config.refetchInterval,
});
if (isLoading) return <WidgetSkeleton />;
if (isError) return <WidgetError />;
return (
<div className="widget-content">
<p className="text-3xl font-bold">{formatCurrency(data.total)}</p>
<p className="text-sm text-muted">vs last period: {data.changePercent}%</p>
</div>
);
}
การอัปเดต Real-time
ตัวเลือก refetchInterval ให้แต่ละ widget ควบคุมอิสระว่าจะ poll ข้อมูลใหม่บ่อยแค่ไหน activity feed อาจต้องการการอัปเดตทุกสิบวินาที สรุปรายได้รายเดือนอาจดีกับการ refresh ครั้งละชั่วโมง การ poll ทั่วโลก — ทางเลือกที่ใช้กันทั่วไป — บังคับให้ทุก widget อยู่บน interval เดียวกัน
สิ่งที่สถาปัตยกรรมนี้ป้องกัน
ปัญหาที่สถาปัตยกรรมนี้ป้องกันมีความสำคัญพอ ๆ กับสิ่งที่มันเปิดใช้งาน Global loading state ไม่จำเป็น — ไม่มีช่วงเวลาที่ "ข้อมูลทั้งหมด" กำลัง load มีแค่ widget แต่ละตัว ความล้มเหลวแบบ cascade เป็นไปไม่ได้ในโครงสร้าง — error ของ widget หนึ่งไม่สามารถแพร่กระจายไปยัง widget อื่นได้เพราะแต่ละ error boundary และแต่ละ query ถูก scope ไปยัง widget เดียว