Patterns
Patterns et bonnes pratiques React/Next.js utilisées dans Wagoo.
Context API
Pour partager l’état entre composants sans prop drilling.
// lib/context/project.tsx
import { createContext, useContext, useState } from "react";
interface ProjectContextType {
projectId: string;
setProjectId: (id: string) => void;
}
const ProjectContext = createContext<ProjectContextType | undefined>(undefined);
export function ProjectProvider({ children }: { children: React.ReactNode }) {
const [projectId, setProjectId] = useState("");
return (
<ProjectContext.Provider value={{ projectId, setProjectId }}>
{children}
</ProjectContext.Provider>
);
}
export function useProject() {
const context = useContext(ProjectContext);
if (!context) {
throw new Error("useProject must be used within ProjectProvider");
}
return context;
}Utilisation :
// app/layout.tsx
import { ProjectProvider } from "@/lib/context/project";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ProjectProvider>
{children}
</ProjectProvider>
);
}
// components/my-component.tsx
import { useProject } from "@/lib/context/project";
export default function MyComponent() {
const { projectId } = useProject();
return <div>Project: {projectId}</div>;
}Form Handling
Utiliser React Hook Form + Zod pour la validation.
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Min 8 characters"),
});
type LoginFormData = z.infer<typeof schema>;
export default function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: LoginFormData) => {
const response = await fetch("/api/auth/sign-in", {
method: "POST",
body: JSON.stringify(data),
});
const json = await response.json();
// Handle response
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<input {...register("password")} type="password" />
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">Login</button>
</form>
);
}Data Fetching (Server Component)
Utiliser async/await dans les Server Components.
// app/projects/[id]/page.tsx
import { prisma } from "@/lib/prisma";
export default async function ProjectPage({ params }: { params: { id: string } }) {
const project = await prisma.project.findUnique({
where: { id: params.id },
});
if (!project) {
return <div>Project not found</div>;
}
return (
<div>
<h1>{project.name}</h1>
<p>{project.description}</p>
</div>
);
}Data Fetching (Client Component)
Utiliser useEffect + fetch pour les Client Components.
"use client";
import { useEffect, useState } from "react";
export default function Articles({ projectId }: { projectId: string }) {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchArticles = async () => {
const response = await fetch(`/api/projects/${projectId}/articles`);
const data = await response.json();
setArticles(data.data);
setLoading(false);
};
fetchArticles();
}, [projectId]);
if (loading) return <div>Loading...</div>;
return (
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}Error Handling
Créer un error boundary pour capturer les erreurs.
// components/error-boundary.tsx
"use client";
import { useEffect } from "react";
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center h-screen">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button
onClick={() => reset()}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
>
Try again
</button>
</div>
);
}Loading States
Afficher un skeleton pendant le chargement.
// components/article-card-skeleton.tsx
export default function ArticleCardSkeleton() {
return (
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mt-2"></div>
</div>
);
}
// app/articles/page.tsx
import { Suspense } from "react";
export default function ArticlesPage() {
return (
<Suspense fallback={<ArticleCardSkeleton />}>
<ArticlesList />
</Suspense>
);
}Modal Pattern
Gérer les modals avec des routes URL.
// app/@modal/(.)article/[id]/page.tsx
import { Modal } from "@/components/modal";
export default function ArticleModal({ params }: { params: { id: string } }) {
return (
<Modal>
<ArticleContent id={params.id} />
</Modal>
);
}Revalidation
Invalider le cache après une mutation.
// app/actions.ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
import { prisma } from "@/lib/prisma";
export async function createArticle(data: any) {
const article = await prisma.article.create({ data });
// Revalidate la page des articles
revalidatePath(`/projects/${data.projectId}/articles`);
// Revalidate le tag spécifique
revalidateTag(`articles-${data.projectId}`);
return article;
}Utilisation :
// components/create-article-form.tsx
import { createArticle } from "@/app/actions";
export default function CreateArticleForm({ projectId }: { projectId: string }) {
async function handleSubmit(formData: FormData) {
await createArticle({
projectId,
title: formData.get("title"),
});
}
return (
<form action={handleSubmit}>
{/* Form fields */}
</form>
);
}Type Safety
Utiliser TypeScript pour la sécurité des types.
// lib/types.ts
export interface Project {
id: string;
name: string;
description?: string;
createdAt: Date;
}
// components/project-card.tsx
interface ProjectCardProps {
project: Project;
}
export default function ProjectCard({ project }: ProjectCardProps) {
return (
<div>
<h2>{project.name}</h2>
</div>
);
}API Route Protection
Vérifier l’authentification dans les API routes.
// lib/auth-middleware.ts
import { auth } from "@/lib/auth";
export async function withAuth(request: Request, handler: Function) {
const session = await auth.getSession(request);
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return handler(request, session);
}
// app/api/projects/route.ts
import { withAuth } from "@/lib/auth-middleware";
export async function GET(request: Request) {
return withAuth(request, async (req, session) => {
const projects = await prisma.project.findMany({
where: { ownerId: session.user.id },
});
return Response.json({ data: projects });
});
}Voir aussi
Voir Composants UI et Layouts pour plus de patterns.
Last updated on