From e10cb2e294c4bf4aaf884ac094c7aebb4514aa77 Mon Sep 17 00:00:00 2001 From: engineering-props-to Date: Sun, 15 Feb 2026 11:22:48 +0000 Subject: [PATCH 1/2] feat: add Suspense boundaries for dashboard streaming - Refactor dashboard page to use Suspense for stats and recent feedback - Create async FeedbackStats component that fetches its own data - Create async RecentFeedbackSection component - Add StatsCardsSkeleton and RecentFeedbackSkeleton loading states - Static Quick Actions section renders immediately while data streams This improves perceived performance by showing the page shell instantly while stats load in the background. Part of #56 --- apps/app/src/app/(dashboard)/page.tsx | 43 ++++++------------- .../src/app/_components/feedback-stats.tsx | 26 +++++++++++ .../_components/recent-feedback-section.tsx | 34 +++++++++++++++ .../_components/recent-feedback-skeleton.tsx | 10 +++++ .../app/_components/stats-cards-skeleton.tsx | 24 +++++++++++ 5 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 apps/app/src/app/_components/feedback-stats.tsx create mode 100644 apps/app/src/app/_components/recent-feedback-section.tsx create mode 100644 apps/app/src/app/_components/recent-feedback-skeleton.tsx create mode 100644 apps/app/src/app/_components/stats-cards-skeleton.tsx diff --git a/apps/app/src/app/(dashboard)/page.tsx b/apps/app/src/app/(dashboard)/page.tsx index 7151df9c..f80988d1 100644 --- a/apps/app/src/app/(dashboard)/page.tsx +++ b/apps/app/src/app/(dashboard)/page.tsx @@ -1,8 +1,10 @@ +import { Suspense } from "react"; import Link from "next/link"; import { auth } from "@/server/auth.server"; -import { FeedbackStatsCards } from "@/app/_components/feedback-stats-cards"; -import { NoFeedbackState } from "@/app/_components/empty-state"; -import { getFeedbackStats } from "@propsto/data/repos"; +import { FeedbackStats } from "@/app/_components/feedback-stats"; +import { StatsCardsSkeleton } from "@/app/_components/stats-cards-skeleton"; +import { RecentFeedbackSection } from "@/app/_components/recent-feedback-section"; +import { RecentFeedbackSkeleton } from "@/app/_components/recent-feedback-skeleton"; export default async function DashboardPage(): Promise { const session = await auth(); @@ -12,17 +14,6 @@ export default async function DashboardPage(): Promise { const userId = session.user.id; - // Get feedback stats for the user - const statsResult = await getFeedbackStats(userId); - const stats = statsResult.success - ? { - received: statsResult.data.received, - sent: statsResult.data.sent, - pending: statsResult.data.pendingModeration, - recentCount: statsResult.data.recentCount, - } - : { received: 0, sent: 0, pending: 0, recentCount: 0 }; - return (
@@ -32,23 +23,15 @@ export default async function DashboardPage(): Promise {

- {/* Stats Cards */} - + {/* Stats Cards - Streamed with Suspense */} + }> + + - {/* Recent Feedback or Empty State */} -
-

Recent Feedback

- {stats.received === 0 ? ( - - ) : ( -
- You have {stats.received} feedback items.{" "} - - View all - -
- )} -
+ {/* Recent Feedback - Streamed with Suspense */} + }> + + {/* Quick Actions */}
diff --git a/apps/app/src/app/_components/feedback-stats.tsx b/apps/app/src/app/_components/feedback-stats.tsx new file mode 100644 index 00000000..21dc63e0 --- /dev/null +++ b/apps/app/src/app/_components/feedback-stats.tsx @@ -0,0 +1,26 @@ +import { getFeedbackStats } from "@propsto/data/repos"; +import { FeedbackStatsCards } from "./feedback-stats-cards"; + +interface FeedbackStatsProps { + userId: string; +} + +/** + * Async server component that fetches stats. + * Wrap with Suspense for streaming. + */ +export async function FeedbackStats({ + userId, +}: FeedbackStatsProps): Promise { + const statsResult = await getFeedbackStats(userId); + const stats = statsResult.success + ? { + received: statsResult.data.received, + sent: statsResult.data.sent, + pending: statsResult.data.pendingModeration, + recentCount: statsResult.data.recentCount, + } + : { received: 0, sent: 0, pending: 0, recentCount: 0 }; + + return ; +} diff --git a/apps/app/src/app/_components/recent-feedback-section.tsx b/apps/app/src/app/_components/recent-feedback-section.tsx new file mode 100644 index 00000000..94a49cae --- /dev/null +++ b/apps/app/src/app/_components/recent-feedback-section.tsx @@ -0,0 +1,34 @@ +import Link from "next/link"; +import { getFeedbackStats } from "@propsto/data/repos"; +import { NoFeedbackState } from "./empty-state"; + +interface RecentFeedbackSectionProps { + userId: string; +} + +/** + * Async component for recent feedback section. + * Wrap with Suspense for streaming. + */ +export async function RecentFeedbackSection({ + userId, +}: RecentFeedbackSectionProps): Promise { + const statsResult = await getFeedbackStats(userId); + const receivedCount = statsResult.success ? statsResult.data.received : 0; + + return ( +
+

Recent Feedback

+ {receivedCount === 0 ? ( + + ) : ( +
+ You have {receivedCount} feedback items.{" "} + + View all + +
+ )} +
+ ); +} diff --git a/apps/app/src/app/_components/recent-feedback-skeleton.tsx b/apps/app/src/app/_components/recent-feedback-skeleton.tsx new file mode 100644 index 00000000..a74635a4 --- /dev/null +++ b/apps/app/src/app/_components/recent-feedback-skeleton.tsx @@ -0,0 +1,10 @@ +import { Skeleton } from "@propsto/ui/atoms/skeleton"; + +export function RecentFeedbackSkeleton(): React.JSX.Element { + return ( +
+ + +
+ ); +} diff --git a/apps/app/src/app/_components/stats-cards-skeleton.tsx b/apps/app/src/app/_components/stats-cards-skeleton.tsx new file mode 100644 index 00000000..05d64bbd --- /dev/null +++ b/apps/app/src/app/_components/stats-cards-skeleton.tsx @@ -0,0 +1,24 @@ +import { Card, CardFooter, CardHeader } from "@propsto/ui/atoms/card"; +import { Skeleton } from "@propsto/ui/atoms/skeleton"; + +export function StatsCardsSkeleton(): React.JSX.Element { + return ( +
+ {[1, 2, 3, 4].map(i => ( + + + + +
+ +
+
+ + + + +
+ ))} +
+ ); +} From 37875b9b91df9d551c22146c28ee4861ccfbe02e Mon Sep 17 00:00:00 2001 From: engineering-props-to Date: Sun, 15 Feb 2026 11:24:34 +0000 Subject: [PATCH 2/2] fix: deduplicate getFeedbackStats with React cache() Use React's cache() to deduplicate the getFeedbackStats call across Suspense boundaries, preventing duplicate database queries. --- apps/app/src/app/_components/cached-queries.ts | 8 ++++++++ apps/app/src/app/_components/feedback-stats.tsx | 2 +- apps/app/src/app/_components/recent-feedback-section.tsx | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 apps/app/src/app/_components/cached-queries.ts diff --git a/apps/app/src/app/_components/cached-queries.ts b/apps/app/src/app/_components/cached-queries.ts new file mode 100644 index 00000000..824ea397 --- /dev/null +++ b/apps/app/src/app/_components/cached-queries.ts @@ -0,0 +1,8 @@ +import { cache } from "react"; +import { getFeedbackStats as _getFeedbackStats } from "@propsto/data/repos"; + +/** + * Cached version of getFeedbackStats. + * Deduplicates calls within the same request (e.g., across Suspense boundaries). + */ +export const getFeedbackStats = cache(_getFeedbackStats); diff --git a/apps/app/src/app/_components/feedback-stats.tsx b/apps/app/src/app/_components/feedback-stats.tsx index 21dc63e0..955eef5a 100644 --- a/apps/app/src/app/_components/feedback-stats.tsx +++ b/apps/app/src/app/_components/feedback-stats.tsx @@ -1,4 +1,4 @@ -import { getFeedbackStats } from "@propsto/data/repos"; +import { getFeedbackStats } from "./cached-queries"; import { FeedbackStatsCards } from "./feedback-stats-cards"; interface FeedbackStatsProps { diff --git a/apps/app/src/app/_components/recent-feedback-section.tsx b/apps/app/src/app/_components/recent-feedback-section.tsx index 94a49cae..0695d55e 100644 --- a/apps/app/src/app/_components/recent-feedback-section.tsx +++ b/apps/app/src/app/_components/recent-feedback-section.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { getFeedbackStats } from "@propsto/data/repos"; +import { getFeedbackStats } from "./cached-queries"; import { NoFeedbackState } from "./empty-state"; interface RecentFeedbackSectionProps {