skills/webdevcody/agentic-jumpstart/agentic-jumpstart-react

agentic-jumpstart-react

SKILL.md

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/
Weekly Installs
3
GitHub Stars
21
First Seen
Feb 3, 2026
Installed on
opencode3
codex3
gemini-cli2
replit2
antigravity2
claude-code2