refactor(client): optimize suspense components #4

Merged
sugar merged 2 commits from noa.virellia/suspense-fallback-optimization into develop 2026-01-01 03:47:56 +00:00
9 changed files with 130 additions and 43 deletions

View File

@@ -7,7 +7,6 @@ import * as React from 'react';
import NixOSLogo from '@/assets/nixos.svg?react';
import { NavMain } from '@/components/nav-main';
import { NavSecondary } from '@/components/nav-secondary';
import { NavUser } from '@/components/nav-user';
import {
Sidebar,
SidebarContent,
@@ -17,6 +16,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { NavUser } from './nav-user';
const data = {
user: {
@@ -48,7 +48,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
className="data-[slot=sidebar-menu-button]:p-1.5!"
>
<a href="#">
<NixOSLogo />

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import {
Dialog,
DialogContent,
@@ -14,9 +15,9 @@ import { Button } from '../ui/button';
export function QrDialog(
{ eventId }: { eventId: string },
) {
const { data } = useCheckinCode(eventId);
const [open, setOpen] = useState(false);
return (
<Dialog>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="w-20"></Button>
</DialogTrigger>
@@ -27,21 +28,41 @@ export function QrDialog(
</DialogDescription>
</DialogHeader>
<QrDialogContent checkinCode={data.data.checkin_code} />
<QrSection eventId={eventId} enabled={open} />
</DialogContent>
</Dialog>
);
}
function QrDialogContent({ checkinCode }: { checkinCode: string }) {
return (
function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean }) {
const { data } = useCheckinCode(eventId, enabled);
return data
? (
<>
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
<QRCode data={checkinCode} className="size-60" />
<QRCode data={data.data.checkin_code} className="size-60" />
</div>
<DialogFooter>
<div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
{checkinCode}
{data.data.checkin_code}
</div>
</DialogFooter>
</>
)
: (
<QrSectionSkeleton />
);
}
function QrSectionSkeleton() {
return (
<>
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
<QRCode data="114514" className="size-60 blur-xs" />
</div>
<DialogFooter>
<div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
Loading...
</div>
</DialogFooter>
</>

View File

@@ -0,0 +1,20 @@
import type { ReactNode } from 'react';
import React, { Suspense } from 'react';
export function withFallback<P extends object>(
Component: React.ComponentType<P>,
fallback: ReactNode,
) {
const Wrapped: React.FC<P> = (props) => {
return (
<Suspense fallback={fallback}>
<Component {...props} />
</Suspense>
);
};
Wrapped.displayName = `withFallback(${Component.displayName! || Component.name || 'Component'
})`;
return Wrapped;
}

View File

@@ -24,8 +24,10 @@ import {
} from '@/components/ui/sidebar';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { useLogout } from '@/hooks/useLogout';
import { withFallback } from './hoc/with-fallback';
import { Skeleton } from './ui/skeleton';
export function NavUser() {
function NavUser_() {
const { isMobile } = useSidebar();
const { data: user } = useUserInfo();
const { logout } = useLogout();
@@ -83,3 +85,20 @@ export function NavUser() {
</SidebarMenu>
);
}
function NavUserSkeleton() {
return (
<SidebarMenuButton
size="lg"
>
<Skeleton className="h-8 w-8 rounded-lg" />
<div className="flex flex-col flex-1 gap-1">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-24" />
</div>
<IconDotsVertical className="ml-auto size-4" />
</SidebarMenuButton>
);
}
export const NavUser = withFallback(NavUser_, <NavUserSkeleton />);

View File

@@ -0,0 +1,9 @@
import { Skeleton } from '../ui/skeleton';
export function CardSkeleton() {
return (
<Skeleton
className="gap-6 rounded-xl py-6 h-full"
/>
);
}

View File

@@ -2,11 +2,12 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { QrDialog } from '../checkin/qr-dialog';
import { withFallback } from '../hoc/with-fallback';
import { CardSkeleton } from './card-skeleton';
export function CheckinCard() {
function CheckinCard_() {
const { data } = useUserInfo();
return (
<>
<Card className="@container/card">
<CardHeader>
<CardDescription></CardDescription>
@@ -25,6 +26,7 @@ export function CheckinCard() {
</QrDialog>
</CardFooter>
</Card>
</>
);
}
export const CheckinCard = withFallback(CheckinCard_, <CardSkeleton />);

View File

@@ -1,8 +1,8 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { axiosClient } from '@/lib/axios';
export function useCheckinCode(eventId: string) {
return useSuspenseQuery({
export function useCheckinCode(eventId: string, enabled: boolean) {
return useQuery({
queryKey: ['getCheckinCode', eventId],
queryFn: async () => {
return axiosClient.get<{
@@ -13,5 +13,6 @@ export function useCheckinCode(eventId: string) {
},
});
},
enabled,
});
}

View File

@@ -4,7 +4,24 @@ import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/sonner';
import '@/index.css';
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
// eslint-disable-next-line ts/no-unsafe-assignment
const status
// eslint-disable-next-line ts/no-unsafe-member-access
= error?.response?.status ?? error?.status;
if (status >= 400 && status < 500) {
return false;
}
return failureCount < 3;
},
},
},
});
function RootLayout() {
return (

View File

@@ -13,18 +13,16 @@ export const Route = createFileRoute('/_sidebarLayout/')({
},
});
function SectionCards() {
return (
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
<CheckinCard />
</div>
);
}
function Index() {
return (
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<SectionCards />
{/* Section Cards */}
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card
grid grid-cols-1 gap-4 px-4 auto-rows-[minmax(175px,auto)] items-stretch
*:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4"
>
<CheckinCard />
</div>
</div>
);
}