AgentSkillsCN

Ui Pattern Shadcn React

Ui Pattern Shadcn React

SKILL.md

Berikut adalah raw code lengkapnya. Anda bisa langsung klik tombol "Copy" di pojok kanan atas blok kode ini dan paste ke dalam file SKILL.md Anda.

markdown
---
name: shadcn-react-patterns
description: Modern React UI patterns using Shadcn UI for loading states, error handling, and data fetching. Best practices for skeleton loading, form validation, and feedback loops.
---

# Shadcn UI React Patterns

## Core Principles

1.  **Never show stale UI** - Use `<Skeleton />` or loading buttons appropriately.
2.  **Always surface errors** - Use `Toast` for transient errors, `Alert` for persistent ones.
3.  **Optimistic updates** - UI reacts instantly, validates in background.
4.  **Visual Consistency** - Use Shadcn design tokens (muted-foreground, destructive) instead of raw colors.

## Loading State Patterns

### The Golden Rule

**Show loading indicator ONLY when there's no data to display.**

```tsx
import { Skeleton } from "@/components/ui/skeleton"
import { Alert, AlertDescription } from "@/components/ui/alert"

// CORRECT - Only show Skeleton when no data exists
const { data, isLoading, error } = useGetItemsQuery();

if (error) {
  return (
    <Alert variant="destructive">
      <AlertDescription>Failed to load data.</AlertDescription>
    </Alert>
  )
}

// Show skeleton only on initial load (loading AND no data)
if (isLoading && !data) {
  return (
    <div className="space-y-2">
      <Skeleton className="h-4 w-[250px]" />
      <Skeleton className="h-4 w-[200px]" />
    </div>
  )
}

if (!data?.items.length) return <EmptyState />;

return <ItemList items={data.items} />;

Loading State Decision Tree

code
Is there an error?
  → Yes: Show <Alert variant="destructive" />
  → No: Continue

Is it loading AND we have no data?
  → Yes: Show <Skeleton /> structure matching content shape
  → No: Continue

Do we have data?
  → Yes, with items: Show the data (DataTable or Cards)
  → Yes, but empty: Show EmptyState (Card or centered layout)
  → No: Show loading fallback

Skeleton vs Loader2 (Spinner)

Use <Skeleton /> WhenUse <Loader2 /> When
Loading initial page contentButton actions (isSubmitting)
Card/Table layoutsModal/Dialog saving states
Preventing layout shift (CLS)Inline minor updates

Error Handling Patterns

The Error Handling Hierarchy

code
1. Inline error → <FormMessage /> (Shadcn Form context)
2. Toast notification → toast.error() (Sonner/Toaster)
3. Error banner → <Alert variant="destructive" />
4. Full error screen → Layout with typography h1/p

Toast Integration (Sonner/Toaster)

CRITICAL: Never swallow errors silently.

tsx
import { toast } from "sonner" // or "@/components/ui/use-toast"

// CORRECT - Error always surfaced via Toast
const [createItem, { isLoading }] = useCreateItemMutation({
  onSuccess: () => {
    toast.success("Item created", {
      description: "Your new item has been added to the list.",
    });
  },
  onError: (error) => {
    console.error(error);
    toast.error("Uh oh! Something went wrong.", {
      description: error.message || "There was a problem with your request.",
      action: {
        label: "Undo",
        onClick: () => console.log("Undo"),
      },
    });
  },
});

Error State Component (Using Alert)

tsx
import { AlertCircle } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

interface ErrorStateProps {
  error: Error;
  onRetry?: () => void;
}

const ErrorState = ({ error, onRetry }: ErrorStateProps) => (
  <Alert variant="destructive" className="my-4">
    <AlertCircle className="h-4 w-4" />
    <AlertTitle>Error</AlertTitle>
    <AlertDescription className="flex flex-col gap-2">
      <p>{error.message}</p>
      {onRetry && (
        <Button variant="outline" size="sm" onClick={onRetry} className="w-fit bg-background text-destructive hover:bg-destructive/10">
          Try Again
        </Button>
      )}
    </AlertDescription>
  </Alert>
);

Button State Patterns

Button Loading State

Use the Loader2 icon from lucide-react with animate-spin.

tsx
import { Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"

// CORRECT - Button disabled and showing spinner
<Button disabled={isSubmitting} type="submit">
  {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
  {isSubmitting ? "Submitting..." : "Submit"}
</Button>

Empty States

Shadcn doesn't have a core "Empty" component, so we compose one using Typography and Icons.

Composable Empty State

tsx
import { FileQuestion } from "lucide-react"

const EmptyState = ({ title, description, action }: any) => (
  <div className="flex flex-col items-center justify-center space-y-3 py-12 text-center border rounded-lg border-dashed bg-muted/10">
    <div className="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
      <FileQuestion className="h-6 w-6 text-muted-foreground" />
    </div>
    <div className="space-y-1">
      <h3 className="text-lg font-medium tracking-tight">{title}</h3>
      <p className="text-sm text-muted-foreground">{description}</p>
    </div>
    {action && <div className="pt-2">{action}</div>}
  </div>
);

// Usage
<EmptyState 
  title="No items found" 
  description="You haven't created any items yet."
  action={<Button>Create Item</Button>} 
/>

Form Patterns (React Hook Form + Zod)

The standard Shadcn pattern uses zod for validation and Form components for layout.

tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Loader2 } from "lucide-react"

const formSchema = z.object({
  username: z.string().min(2, { message: "Username must be at least 2 characters." }),
})

export function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { username: "" },
  })

  // Loading state comes from the mutation hook or form state
  const isSubmitting = form.formState.isSubmitting;

  function onSubmit(values: z.infer<typeof formSchema>) {
    // API Call here
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                {/* Error styling is handled automatically by shadcn Input */}
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              {/* Inline Error Message */}
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" disabled={isSubmitting}>
          {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
          Submit
        </Button>
      </form>
    </Form>
  )
}

Anti-Patterns

Loading States

tsx
// WRONG - Blocking the UI with a full-screen spinner for minor updates
if (isFetching) return <FullPageSpinner />;

// CORRECT - Show opacity or a small spinner next to the title
<div className={cn("transition-opacity", isFetching && "opacity-50 pointer-events-none")}>
  <DataTable data={data} />
</div>

Feedback

tsx
// WRONG - Using native browser alert
alert("Error saving data");

// CORRECT - Using Shadcn Toast
toast.error("Error saving data");

Checklist

Before completing any Shadcn UI component:

UI States:

  • Error state uses <Alert variant="destructive"> or toast.error
  • Loading state uses <Skeleton /> matching the content shape
  • Empty state uses styled container with text-muted-foreground
  • Buttons have disabled={isLoading} and <Loader2 />

Forms:

  • Uses <Form> wrapper with zod schema
  • Field errors map to <FormMessage />
  • Submit button shows loading state
code