Claude Code Plugins

Community-maintained marketplace

Feedback

agentic-jumpstart-react

@webdevcody/agentic-jumpstart
12
0

React 19 patterns and best practices for TanStack Start applications with shadcn/ui, React Hook Form, Zod, Framer Motion, and TanStack Query. Use when building React components, forms, handling state, data fetching, animations, drag and drop, toasts, modals, or when the user mentions React, hooks, components, or UI.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name agentic-jumpstart-react
description React 19 patterns and best practices for TanStack Start applications with shadcn/ui, React Hook Form, Zod, Framer Motion, and TanStack Query. Use when building React components, forms, handling state, data fetching, animations, drag and drop, toasts, modals, or when the user mentions React, hooks, components, or UI.

React 19 Best Practices

Component Structure

File Organization

src/routes/admin/courses/
├── route.tsx              # Route definition
├── -components/           # Route-specific components
│   ├── CourseList.tsx
│   ├── CourseForm.tsx
│   └── CourseCard.tsx

Component Pattern

import { type ReactNode } from "react";

interface CourseCardProps {
  course: Course;
  onEdit?: (id: number) => void;
  children?: ReactNode;
}

export function CourseCard({ course, onEdit, children }: CourseCardProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{course.title}</CardTitle>
        <CardDescription>{course.description}</CardDescription>
      </CardHeader>
      <CardContent>{children}</CardContent>
      {onEdit && (
        <CardFooter>
          <Button onClick={() => onEdit(course.id)}>Edit</Button>
        </CardFooter>
      )}
    </Card>
  );
}

Form Handling with React Hook Form + Zod

Basic Form Pattern

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const formSchema = z.object({
  title: z.string().min(1, "Title is required").max(100),
  description: z.string().max(500).optional(),
  isPremium: z.boolean().default(false),
});

type FormData = z.infer<typeof formSchema>;

export function CourseForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      description: "",
      isPremium: false,
    },
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <Label htmlFor="title">Title</Label>
        <Input
          id="title"
          {...form.register("title")}
          aria-invalid={!!form.formState.errors.title}
        />
        {form.formState.errors.title && (
          <p className="text-sm text-red-500">
            {form.formState.errors.title.message}
          </p>
        )}
      </div>

      <div>
        <Label htmlFor="description">Description</Label>
        <Textarea id="description" {...form.register("description")} />
      </div>

      <div className="flex items-center gap-2">
        <Checkbox
          id="isPremium"
          checked={form.watch("isPremium")}
          onCheckedChange={(checked) =>
            form.setValue("isPremium", checked === true)
          }
        />
        <Label htmlFor="isPremium">Premium content</Label>
      </div>

      <Button type="submit" disabled={form.formState.isSubmitting}>
        {form.formState.isSubmitting ? "Saving..." : "Save"}
      </Button>
    </form>
  );
}

Data Fetching with TanStack Query

Query Options Pattern

import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query";

// Define query options (can be shared between components)
export const courseQueryOptions = (courseId: string) =>
  queryOptions({
    queryKey: ["course", courseId],
    queryFn: () => getCourseFn({ data: { courseId } }),
  });

// In component - with suspense
function CourseDetails({ courseId }: { courseId: string }) {
  const { data: course } = useSuspenseQuery(courseQueryOptions(courseId));
  return <div>{course.title}</div>;
}

// In component - without suspense
function CourseDetails({ courseId }: { courseId: string }) {
  const { data: course, isLoading, error } = useQuery(courseQueryOptions(courseId));

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorDisplay error={error} />;
  return <div>{course.title}</div>;
}

Mutations with Optimistic Updates

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";

export function useUpdateCourse() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateCourseFn,
    onSuccess: () => {
      toast.success("Course updated successfully");
      queryClient.invalidateQueries({ queryKey: ["courses"] });
    },
    onError: (error) => {
      toast.error(error.message || "Failed to update course");
    },
  });
}

Loading States & Suspense

Loading Skeleton Pattern

function CourseSkeleton() {
  return (
    <Card>
      <CardHeader>
        <Skeleton className="h-6 w-3/4" />
        <Skeleton className="h-4 w-1/2" />
      </CardHeader>
      <CardContent>
        <Skeleton className="h-20 w-full" />
      </CardContent>
    </Card>
  );
}

function CourseList() {
  return (
    <Suspense fallback={<CourseSkeleton />}>
      <CourseListContent />
    </Suspense>
  );
}

Error Boundaries

Route-Level Error Handling

import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";

export const Route = createFileRoute("/courses/$courseId")({
  component: CoursePage,
  errorComponent: DefaultCatchBoundary,
});

Dialog & Modal Pattern

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "~/components/ui/dialog";

function DeleteCourseDialog({
  course,
  onDelete,
}: {
  course: Course;
  onDelete: () => void;
}) {
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button variant="destructive">Delete</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Delete Course</DialogTitle>
          <DialogDescription>
            Are you sure you want to delete "{course.title}"? This action cannot
            be undone.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            variant="destructive"
            onClick={() => {
              onDelete();
              setOpen(false);
            }}
          >
            Delete
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Toast Notifications with Sonner

import { toast } from "sonner";

// Success toast
toast.success("Course saved successfully");

// Error toast
toast.error("Failed to save course");

// Promise toast
toast.promise(saveCourse(data), {
  loading: "Saving course...",
  success: "Course saved!",
  error: "Failed to save course",
});

// Custom toast with action
toast("Course deleted", {
  action: {
    label: "Undo",
    onClick: () => restoreCourse(courseId),
  },
});

Animation with Framer Motion

Basic Animation

import { motion } from "framer-motion";

function FadeInCard({ children }: { children: ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{ duration: 0.2 }}
    >
      {children}
    </motion.div>
  );
}

Staggered List Animation

import { motion } from "framer-motion";

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: { staggerChildren: 0.1 },
  },
};

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
};

function AnimatedList({ items }: { items: Item[] }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {items.map((item) => (
        <motion.li key={item.id} variants={item}>
          {item.title}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Drag and Drop with @hello-pangea/dnd

import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

function ReorderableList({
  items,
  onReorder,
}: {
  items: Item[];
  onReorder: (items: Item[]) => void;
}) {
  const handleDragEnd = (result: DropResult) => {
    if (!result.destination) return;

    const reordered = Array.from(items);
    const [removed] = reordered.splice(result.source.index, 1);
    reordered.splice(result.destination.index, 0, removed);

    onReorder(reordered);
  };

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="items">
        {(provided) => (
          <ul {...provided.droppableProps} ref={provided.innerRef}>
            {items.map((item, index) => (
              <Draggable key={item.id} draggableId={String(item.id)} index={index}>
                {(provided) => (
                  <li
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {item.title}
                  </li>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </ul>
        )}
      </Droppable>
    </DragDropContext>
  );
}

Custom Hooks

useLocalStorage

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

useDebounce

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

Component Patterns Checklist

  • Components use TypeScript interfaces for props
  • Forms use React Hook Form with Zod validation
  • Data fetching uses TanStack Query patterns
  • Loading states have skeleton placeholders
  • Dialogs use shadcn/ui Dialog component
  • Toasts use Sonner for notifications
  • Animations use Framer Motion
  • Effects clean up subscriptions
  • Route-specific components in -components/ subdirectory
  • Shared components in /src/components/