Claude-skill-registry Atomic Component Generator
Génère des composants React suivant l'Atomic Design avec décomposition maximale et pattern Smart/Dumb. MANDATORY pour tous composants. À utiliser lors de la création de composants, pages, ou quand l'utilisateur mentionne "component", "atomic", "smart", "dumb", "decompose", "React", "UI".
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/atomic-component" ~/.claude/skills/majiayu000-claude-skill-registry-atomic-component-generator && rm -rf "$T"
skills/data/atomic-component/SKILL.mdAtomic Component Generator
🎯 Mission
Créer des composants React maximalement décomposés suivant l'Atomic Design et le pattern Smart/Dumb pour une réutilisabilité et maintenabilité maximales.
⚛️ Philosophie Atomic Design
Le Principe
Atomic Design décompose l'UI en 5 niveaux :
- Atoms (Atomes) : Plus petites unités (Button, Input, Label)
- Molecules (Molécules) : Groupes d'atomes (FormField = Label + Input + Error)
- Organisms (Organismes) : Groupes de molécules (Header = Logo + Nav + UserMenu)
- Templates : Layouts avec placeholders
- Pages : Templates avec données réelles
Dans ce projet, nous utilisons principalement 3 niveaux :
- Atoms : Composants de base (
- shadcn/ui)components/ui/ - Smart Components : Logique + état (
,features/*/components/*Form
)*List - Dumb Components : Présentation pure (
,features/*/components/*Step
)*Card
Règle d'Or : Décomposition Maximale
CRITICAL : Components MUST be maximally decomposed.
Pourquoi :
- ✅ Réutilisabilité : Petits composants = réutilisables partout
- ✅ Testabilité : Petits composants = faciles à tester
- ✅ Maintenabilité : Un composant = Une responsabilité
- ✅ Lisibilité : Code clair et compréhensible
- ✅ Collaboration : Équipe peut travailler en parallèle
Mauvais signe :
- ❌ Fichier > 150 lignes
- ❌ Composant fait plusieurs choses
- ❌ JSX imbriqué sur 10+ niveaux
- ❌ Logique + présentation mélangées
🧩 Pattern Server/Client Components (Next.js 16)
🎯 Server Components par défaut
IMPORTANT : Depuis Next.js 16, TOUS les composants sont Server Components par défaut.
Server Components :
- ✅ Fetch data server-side (async/await)
- ✅ Accès direct à la base de données / APIs backend
- ✅ Zero JavaScript client-side pour le data fetching
- ✅ Meilleur SEO (contenu pré-rendu)
- ✅ Pas besoin de "use client"
Quand utiliser :
- Pages, layouts, templates
- Widgets qui affichent des données fetchées
- Composants sans interactivité
Exemple :
// Server Component (par défaut, pas de "use client") export async function TeamsWidget() { const teams = await getTeams(); // Fetch server-side return <div>{teams.map(t => <TeamCard key={t.id} team={t} />)}</div>; }
🖱️ Client Components (uniquement si nécessaire)
Client Components (avec
"use client") :
- ✅ Interactivité (onClick, onChange, etc.)
- ✅ Hooks React (useState, useEffect, useContext)
- ✅ Stores Zustand (state client)
- ✅ Browser APIs (localStorage, etc.)
Quand utiliser :
- Formulaires interactifs
- Composants avec state UI (modals, dropdowns)
- Event handlers
- useOptimistic, useTransition
Exemple :
"use client"; // Requis pour Client Component export function TeamForm() { const [name, setName] = useState(""); return <input value={name} onChange={(e) => setName(e.target.value)} />; }
📦 Pattern de Composition Server → Client
Règle d'or : Server Component parent → Client Components enfants
// ✅ BON - Server Component avec fetch export async function TeamsList() { const teams = await getTeams(); // Server-side return ( <div> {teams.map(team => ( <TeamCard key={team.id} team={team} /> // Client si interactivité ))} </div> ); } // Client Component si besoin d'interactivité "use client"; export function TeamCard({ team }: Props) { const [isExpanded, setIsExpanded] = useState(false); return ( <div onClick={() => setIsExpanded(!isExpanded)}> {/* ... */} </div> ); }
⚠️ CRITICAL: Component Folder Architecture
❌ NEVER Create Components in app/
app/RÈGLE ABSOLUE : Le dossier
app/ est uniquement pour le routing Next.js.
❌ MAUVAIS (NEVER DO THIS): app/ └── players/ ├── page.tsx └── components/ ← ❌ JAMAIS DE COMPOSANTS ICI ├── PlayerCard.tsx ← ❌ MAUVAIS ├── PlayersList.tsx ← ❌ MAUVAIS └── ... ✅ BON: app/ └── players/ └── page.tsx ← Routing only (max 50 lignes) features/ └── players/ ← ✅ TOUS les composants vont ici ├── components/ │ ├── PlayerCard.tsx ← ✅ BON │ ├── PlayersList.tsx ← ✅ BON │ └── ... ├── api/ │ └── players.server.ts └── types/
Pourquoi Cette Règle Est Critique
-
Séparation des responsabilités
= Routing Next.js (infrastructure)app/
= Business logic (métier)features/
-
Réutilisabilité
- Composants dans
peuvent être utilisés dans plusieurs routesfeatures/ - Composants dans
sont isolés à une seule routeapp/
- Composants dans
-
Maintenabilité
- Structure claire et prévisible
- Évite la duplication de code
-
Best Practices Next.js 16
- Pages dans
doivent être minces (orchestration)app/ - Logique métier dans
features/
- Pages dans
Examples Réels
// ❌ MAUVAIS // app/players/components/PlayerCard.tsx export function PlayerCard({ player }) { ... } // ❌ MAUVAIS // app/teams/[id]/components/TeamDetails.tsx export function TeamDetails({ team }) { ... } // ✅ BON // features/players/components/PlayerCard.tsx export function PlayerCard({ player }) { ... } // ✅ BON // features/teams/components/TeamDetails.tsx export function TeamDetails({ team }) { ... }
Page Structure
// app/players/page.tsx (Max 50 lignes - Orchestration uniquement) import { PlayersListServer } from "@/features/players/components/PlayersListServer"; import { PlayersStatsGrid } from "@/features/players/components/PlayersStatsGrid"; export default async function PlayersPage() { return ( <div> <Suspense fallback={<StatsGridSkeleton />}> <PlayersStatsGrid /> </Suspense> <Suspense fallback={<ListSkeleton />}> <PlayersListServer /> </Suspense> </div> ); }
🧩 Pattern Smart/Dumb Components (pour Client Components)
Smart Components (Container Components)
Responsabilités :
- ✅ Gèrent l'état client (useState, Zustand stores)
- ✅ Gèrent la logique UI
- ✅ Appellent les Server Actions (mutations)
- ✅ Orchestrent les Dumb Components
- ✅ Gèrent les side effects (useEffect pour polling, refetch, etc.)
Caractéristiques :
- "use client" directive
- Nom se termine par Form, Manager, Container
- Peu ou pas de JSX (délègue aux Dumb Components)
- Beaucoup de logique JavaScript
- Props minimales (reçoit peu, délègue beaucoup)
Emplacement :
features/*/components/ (ex: ClubCreationForm, TeamManager)
Dumb Components (Presentational Components)
Responsabilités :
- ✅ Affichent l'UI (JSX uniquement)
- ✅ Reçoivent des props
- ✅ Émettent des events (callbacks)
- ✅ Aucun état (ou état UI minimal: hover, focus)
- ✅ Aucune logique métier
Caractéristiques :
- Nom descriptif : Step, Card, Item, Display, Section
- Beaucoup de JSX
- Peu ou pas de logique JavaScript
- Props strictement typées (interfaces)
- Pure functions (même props = même output)
Emplacement :
features/*/components/ ou components/shared/ si réutilisable
Exemple de Séparation
// ❌ MAUVAIS - Tout dans un seul composant (150+ lignes) export function ClubCreation() { const [step, setStep] = useState(1); const [clubData, setClubData] = useState({}); const [isPending, startTransition] = useTransition(); const handleSubmit = async () => { startTransition(async () => { await createClubAction(clubData); }); }; return ( <div> {step === 1 && ( <div> <h2>Informations du club</h2> <input onChange={(e) => setClubData({...clubData, name: e.target.value})} /> {/* 50 lignes de formulaire */} </div> )} {step === 2 && ( <div> <h2>Choisir un plan</h2> {/* 50 lignes de sélection de plan */} </div> )} {/* ... */} </div> ); } // ✅ BON - Décomposé en Smart + Dumb // Smart Component (orchestration) export function ClubCreationForm() { const [step, setStep] = useState(1); const { clubData, updateClubData } = useClubCreationStore(); const [isPending, startTransition] = useTransition(); const handleSubmit = async () => { startTransition(async () => { await createClubAction(clubData); }); }; return ( <FormWizard currentStep={step} onStepChange={setStep}> {step === 1 && ( <ClubInfoStep data={clubData} onChange={updateClubData} onNext={() => setStep(2)} /> )} {step === 2 && ( <PlanSelectionStep selectedPlan={clubData.plan} onSelectPlan={(plan) => updateClubData({ plan })} onNext={handleSubmit} isPending={isPending} /> )} </FormWizard> ); } // Dumb Components (présentation) interface ClubInfoStepProps { data: Partial<ClubData>; onChange: (data: Partial<ClubData>) => void; onNext: () => void; } export function ClubInfoStep({ data, onChange, onNext }: ClubInfoStepProps) { return ( <div className="space-y-4"> <h2 className="text-2xl font-bold">Informations du club</h2> <FormField label="Nom du club" value={data.name} onChange={(name) => onChange({ name })} /> <FormField label="Description" value={data.description} onChange={(description) => onChange({ description })} /> <Button onClick={onNext}>Suivant</Button> </div> ); }
📏 Règles de Décomposition
1. Limite de Lignes
- Page : MAX 50 lignes (composition uniquement)
- Smart Component : MAX 100 lignes (logique + orchestration)
- Dumb Component : MAX 80 lignes (présentation)
- Atomic Component : MAX 50 lignes (UI basique)
Si dépassé → Décomposer immédiatement
2. Single Responsibility Principle
Un composant = UNE responsabilité
// ❌ MAUVAIS - Responsabilités multiples export function UserProfile() { // 1. Fetch data // 2. Display profile // 3. Edit form // 4. Settings panel // ... 200 lignes } // ✅ BON - Responsabilités séparées export function UserProfilePage() { return ( <> <UserProfileHeader /> <UserProfileDetails /> <UserProfileSettings /> </> ); } export function UserProfileHeader() { /* ... */ } export function UserProfileDetails() { /* ... */ } export function UserProfileSettings() { /* ... */ }
3. Extraction de la Logique
Règle : Si logique > 10 lignes → Extraire dans un custom hook ou util
// ❌ MAUVAIS - Logique dans le composant export function ClubCreationForm() { const [step, setStep] = useState(1); const [clubData, setClubData] = useState({}); const validateStep1 = () => { if (!clubData.name || clubData.name.length < 3) return false; if (!clubData.description) return false; return true; }; const validateStep2 = () => { // 20 lignes de validation }; // ... logique complexe } // ✅ BON - Logique extraite export function ClubCreationForm() { const { step, clubData, goToNextStep, goToPreviousStep, updateClubData, validateCurrentStep, } = useClubCreationFlow(); return ( <FormWizard currentStep={step}> {/* Présentation pure */} </FormWizard> ); } // Custom hook (logique extraite) function useClubCreationFlow() { const [step, setStep] = useState(1); const [clubData, setClubData] = useState({}); const validateCurrentStep = () => { return clubCreationValidators[step](clubData); }; // ... logique return { step, clubData, goToNextStep, goToPreviousStep, updateClubData, validateCurrentStep, }; }
4. Composition > Monolithic
Privilégier la composition de petits composants
// ❌ MAUVAIS - Composant monolithique export function MemberCard({ member }: Props) { return ( <div className="card"> <div className="avatar"> <img src={member.avatar} /> </div> <div className="info"> <h3>{member.name}</h3> <p>{member.email}</p> <span className="role">{member.role}</span> </div> <div className="actions"> <button onClick={onEdit}>Edit</button> <button onClick={onDelete}>Delete</button> </div> </div> ); } // ✅ BON - Composé de petits composants export function MemberCard({ member, onEdit, onDelete }: Props) { return ( <Card> <MemberAvatar src={member.avatar} alt={member.name} /> <MemberInfo name={member.name} email={member.email} role={member.role} /> <MemberActions onEdit={onEdit} onDelete={onDelete} /> </Card> ); } // Composants réutilisables function MemberAvatar({ src, alt }: AvatarProps) { /* ... */ } function MemberInfo({ name, email, role }: InfoProps) { /* ... */ } function MemberActions({ onEdit, onDelete }: ActionsProps) { /* ... */ }
🏗️ Structure des Composants
Organisation des Dossiers
features/ └── club-management/ └── components/ ├── ClubCreationForm.tsx # Smart (orchestration) ├── ClubInfoStep.tsx # Dumb (présentation step 1) ├── PlanSelectionStep.tsx # Dumb (présentation step 2) ├── MembersList.tsx # Smart (fetch + state) ├── MemberCard.tsx # Dumb (présentation membre) ├── MemberAvatar.tsx # Dumb (avatar) ├── MemberActions.tsx # Dumb (boutons actions) └── shared/ ├── FormWizard.tsx # Réutilisable (wizard) └── StepIndicator.tsx # Réutilisable (steps) components/ └── ui/ # Atomic components (shadcn/ui) ├── button.tsx ├── input.tsx ├── card.tsx └── ...
Template Server Component (preferred)
// features/club-management/components/MembersList.tsx // Server Component (pas de "use client") import { getMembers } from '../api/members.server'; import { MemberCard } from './MemberCard'; import { MembersListSkeleton } from './MembersListSkeleton'; interface MembersListProps { clubId: string; } export async function MembersList({ clubId }: MembersListProps) { // ✅ Fetch server-side const members = await getMembers(clubId); // Empty state if (members.length === 0) { return <EmptyMembersList />; } // Render (délègue aux Client Components si besoin d'interactivité) return ( <div className="space-y-4"> <MembersListHeader count={members.length} /> <div className="grid gap-4"> {members.map(member => ( <MemberCard key={member.id} member={member} clubId={clubId} /> ))} </div> </div> ); }
Template Client Component (si interactivité nécessaire)
// features/club-management/components/MemberCard.tsx "use client"; import { useTransition } from 'react'; import { removeMemberAction } from '../actions/remove-member.action'; interface MemberCardProps { member: Member; clubId: string; } export function MemberCard({ member, clubId }: MemberCardProps) { const [isPending, startTransition] = useTransition(); // Event handler (mutation) const handleRemove = () => { startTransition(async () => { await removeMemberAction(clubId, member.id); }); }; return ( <Card> <MemberInfo member={member} /> <Button onClick={handleRemove} disabled={isPending}> {isPending ? "Suppression..." : "Retirer"} </Button> </Card> ); }
Template Dumb Component
// features/club-management/components/MemberCard.tsx import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { MemberAvatar } from './MemberAvatar'; interface MemberCardProps { member: { id: string; name: string; email: string; role: string; avatar?: string; }; onRemove: () => void; } export function MemberCard({ member, onRemove }: MemberCardProps) { return ( <Card className="p-4 flex items-center gap-4"> <MemberAvatar src={member.avatar} name={member.name} /> <div className="flex-1"> <h3 className="font-semibold">{member.name}</h3> <p className="text-sm text-muted-foreground">{member.email}</p> <span className="text-xs text-primary">{member.role}</span> </div> <Button variant="destructive" size="sm" onClick={onRemove} > Retirer </Button> </Card> ); } // Sous-composant (si réutilisable ailleurs) interface MemberAvatarProps { src?: string; name: string; } function MemberAvatar({ src, name }: MemberAvatarProps) { const initials = name .split(' ') .map(n => n[0]) .join('') .toUpperCase(); if (src) { return ( <img src={src} alt={name} className="w-12 h-12 rounded-full object-cover" /> ); } return ( <div className="w-12 h-12 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold"> {initials} </div> ); }
Template Page (Ultra-thin)
// app/(dashboard)/coach/clubs/[id]/members/page.tsx import { Suspense } from 'react'; import { MembersList } from '@/features/club-management/components/MembersList'; import { MembersListSkeleton } from '@/features/club-management/components/MembersListSkeleton'; interface PageProps { params: { id: string }; } export default function ClubMembersPage({ params }: PageProps) { return ( <div className="container py-8"> <h1 className="text-3xl font-bold mb-6">Membres du club</h1> <Suspense fallback={<MembersListSkeleton />}> <MembersList clubId={params.id} /> </Suspense> </div> ); }
✅ Checklist Atomic Design
Avant de Créer un Composant
- Le composant a-t-il UNE seule responsabilité ?
- Est-il Smart (logique) ou Dumb (présentation) ?
- Peut-il être décomposé davantage ?
- Est-il réutilisable ailleurs ?
- Les props sont-elles strictement typées ?
- Le nom est-il descriptif ?
Pendant la Création
- Logique extraite dans hooks/utils si > 10 lignes
- JSX imbrication < 5 niveaux
- Composant < 150 lignes
- Aucun state dans Dumb Component (sauf UI: hover, focus)
- Aucune logique métier dans Dumb Component
- Composition privilégiée
Après la Création
- Composant testé (si Smart)
- Props documentées (JSDoc si nécessaire)
- Accessible (ARIA, keyboard navigation)
- Responsive (mobile-first)
- Pas de warnings ESLint/TypeScript
🚨 Erreurs Courantes à Éviter
1. Composant Monolithique
// ❌ MAUVAIS - 300 lignes, fait tout export function ClubDashboard() { // Fetch data // Display stats // Display members list // Display latest activities // Display settings panel // ... 300 lignes } // ✅ BON - Décomposé export function ClubDashboard() { return ( <> <ClubStats /> <MembersList /> <ActivitiesFeed /> <SettingsPanel /> </> ); }
2. Mélange Smart/Dumb
// ❌ MAUVAIS - Mélange logique + présentation export function MemberCard({ member }: Props) { const [isPending, startTransition] = useTransition(); const handleRemove = async () => { startTransition(async () => { await removeMemberAction(member.id); }); }; return <Card>...</Card>; // Présentation } // ✅ BON - Séparation claire // Smart Component (logique) export function MembersList() { const handleRemove = async (id: string) => { await removeMemberAction(id); }; return members.map(m => ( <MemberCard member={m} onRemove={() => handleRemove(m.id)} /> )); } // Dumb Component (présentation) export function MemberCard({ member, onRemove }: Props) { return <Card onClick={onRemove}>...</Card>; }
3. Props Drilling Excessif
// ❌ MAUVAIS - Props drilling sur 5 niveaux <GrandParent data={data}> <Parent data={data}> <Child data={data}> <GrandChild data={data} /> </Child> </Parent> </GrandParent> // ✅ BON - Context ou Zustand store const useDataStore = create((set) => ({ data: null, setData: (data) => set({ data }), })); // Composants consomment directement export function GrandChild() { const data = useDataStore(state => state.data); return <div>{data}</div>; }
📚 Skills Complémentaires
Pour aller plus loin :
- mobile-first : Design mobile-first principles
- react-state-management : State management patterns
- zero-warnings : Quality standards
Rappel : Décomposition maximale = Code maintenable, réutilisable et testable. Un composant = Une responsabilité.