← Back to list

agentic-jumpstart-react
by webdevcody
⭐ 15🍴 10📅 Jan 22, 2026
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/
Score
Total Score
65/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
✓フォーク
10回以上フォークされている
+5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon