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