Skip to Content
Wagoo SaaS 1.0.25 is released 🎉
05 ComponentsPatternsPatterns

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> ); }

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