AgentSkillsCN

react-conventions

React与前端的标准。适用于“react component”、“frontend”、“ui”、“hook”、“jsx”等场景。

SKILL.md
--- frontmatter
name: react-conventions
description: Standards React et frontend. Use when "react component", "frontend", "ui", "hook", "jsx".
allowed-tools: Read, Grep, Glob

React Conventions

Purpose

Définir les standards React pour le frontend du projet consultant-manager.

Stack Frontend

  • Framework: React 18.3+
  • Build: Vite
  • Router: React Router 7
  • Styling: Tailwind CSS
  • Dates: date-fns
  • TypeScript: Strict mode

Structure des Composants

Anatomie d'un Composant

typescript
// 1. Imports groupés
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { formatDate } from '../utils/format';
import { consultantsAPI } from '../services/api';
import type { Consultant } from '../types';

// 2. Types/Interfaces
interface ConsultantCardProps {
  consultant: Consultant;
  onEdit?: (consultant: Consultant) => void;
  onDelete?: (id: string) => void;
}

// 3. Composant
export default function ConsultantCard({
  consultant,
  onEdit,
  onDelete
}: ConsultantCardProps) {
  // 4. Hooks (dans l'ordre: state, effects, context, custom hooks)
  const [loading, setLoading] = useState(false);
  const navigate = useNavigate();

  useEffect(() => {
    // Side effects
  }, []);

  // 5. Event handlers
  const handleEdit = () => {
    onEdit?.(consultant);
  };

  const handleDelete = async () => {
    if (!confirm('Êtes-vous sûr ?')) return;
    setLoading(true);
    try {
      await consultantsAPI.delete(consultant.id);
      onDelete?.(consultant.id);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  // 6. Render conditions
  if (loading) {
    return <div>Chargement...</div>;
  }

  // 7. JSX
  return (
    <div className="bg-white shadow rounded-lg p-4">
      <h3 className="text-lg font-semibold">
        {consultant.prenom} {consultant.nom}
      </h3>
      <p className="text-gray-600">{consultant.email}</p>
      <div className="mt-4 flex gap-2">
        <button onClick={handleEdit} className="btn-primary">
          Modifier
        </button>
        <button onClick={handleDelete} className="btn-danger">
          Supprimer
        </button>
      </div>
    </div>
  );
}

Conventions de Nommage

Fichiers

  • Composants: PascalCase.tsx (ex: ConsultantForm.tsx)
  • Hooks: useCamelCase.ts (ex: useConsultants.ts)
  • Utils: camelCase.ts (ex: formatDate.ts)
  • Pages: PascalCase.tsx (ex: Dashboard.tsx)

Variables

typescript
// Composants: PascalCase
const ConsultantCard = () => {};

// Fonctions: camelCase
const handleSubmit = () => {};

// Constantes: UPPER_SNAKE_CASE (si vraiment constantes)
const MAX_FILE_SIZE = 5 * 1024 * 1024;

// État: camelCase descriptif
const [isLoading, setIsLoading] = useState(false);
const [consultants, setConsultants] = useState<Consultant[]>([]);

// Event handlers: handle{Action}
const handleClick = () => {};
const handleSubmit = () => {};
const handleDelete = () => {};

Hooks

Ordre des Hooks

typescript
function MyComponent() {
  // 1. State hooks
  const [data, setData] = useState<Data[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 2. Router hooks
  const navigate = useNavigate();
  const params = useParams();
  const location = useLocation();

  // 3. Effect hooks
  useEffect(() => {
    loadData();
  }, []);

  // 4. Custom hooks
  const { user } = useAuth();
  const { consultants } = useConsultants();

  // ...
}

Custom Hooks

typescript
// useConsultants.ts
import { useState, useEffect } from 'react';
import { consultantsAPI } from '../services/api';
import type { Consultant } from '../types';

export function useConsultants(filters?: { statut?: string }) {
  const [consultants, setConsultants] = useState<Consultant[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let isMounted = true;

    async function load() {
      try {
        setLoading(true);
        const data = await consultantsAPI.getAll(filters);
        if (isMounted) {
          setConsultants(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err instanceof Error ? err.message : 'Erreur');
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    }

    load();

    return () => {
      isMounted = false;
    };
  }, [filters?.statut]);

  const refresh = () => {
    // Re-déclencher le useEffect
  };

  return { consultants, loading, error, refresh };
}

// Utilisation
function ConsultantsList() {
  const { consultants, loading, error } = useConsultants({ statut: 'DISPONIBLE' });

  if (loading) return <div>Chargement...</div>;
  if (error) return <div>Erreur: {error}</div>;

  return <div>{/* ... */}</div>;
}

Gestion d'État

État Local (useState)

typescript
// Pour état simple, local à un composant
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState<FormData>({});

État Dérivé (useMemo)

typescript
// Calculer depuis les props/state
const sortedConsultants = useMemo(() => {
  return consultants.sort((a, b) => a.nom.localeCompare(b.nom));
}, [consultants]);

const filteredMissions = useMemo(() => {
  return missions.filter(m => m.statutFacturation === 'PAYEE');
}, [missions]);

État de Formulaire

typescript
function ConsultantForm({ consultant, onClose }: Props) {
  const [formData, setFormData] = useState({
    nom: consultant?.nom || '',
    prenom: consultant?.prenom || '',
    email: consultant?.email || '',
    tjm: consultant?.tjm || 0,
  });

  const handleChange = (field: string, value: any) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    // Valider et soumettre
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.nom}
        onChange={(e) => handleChange('nom', e.target.value)}
      />
      {/* ... */}
    </form>
  );
}

Appels API

Pattern Chargement/Erreur/Données

typescript
function ConsultantsList() {
  const [consultants, setConsultants] = useState<Consultant[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    loadConsultants();
  }, []);

  const loadConsultants = async () => {
    try {
      setLoading(true);
      setError(null);
      const data = await consultantsAPI.getAll();
      setConsultants(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Erreur de chargement');
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="text-gray-500">Chargement...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded-lg p-4">
        <p className="text-red-800">{error}</p>
        <button onClick={loadConsultants} className="mt-2 btn-primary">
          Réessayer
        </button>
      </div>
    );
  }

  return (
    <div>
      {consultants.map(consultant => (
        <ConsultantCard key={consultant.id} consultant={consultant} />
      ))}
    </div>
  );
}

Tailwind CSS

Principes

  • Utiliser les classes utilitaires uniquement
  • Pas de CSS custom (sauf cas exceptionnel)
  • Responsive design avec préfixes (sm:, md:, lg:)
  • Dark mode prévu avec dark: (si applicable)

Patterns Communs

Boutons:

tsx
// Primary
<button className="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50">
  Action
</button>

// Secondary
<button className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
  Annuler
</button>

// Danger
<button className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">
  Supprimer
</button>

Cards:

tsx
<div className="bg-white shadow rounded-lg p-6">
  {/* Contenu */}
</div>

Forms:

tsx
<input
  type="text"
  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
/>

Badges:

tsx
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
  Disponible
</span>

Responsive

tsx
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
  {/* 1 col mobile, 2 tablet, 4 desktop */}
</div>

Routing

Routes

typescript
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Dashboard />} />
          <Route path="consultants" element={<Consultants />} />
          <Route path="missions" element={<Missions />} />
          <Route path="calendar" element={<Calendar />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

Navigation

typescript
import { Link, useNavigate } from 'react-router-dom';

function MyComponent() {
  const navigate = useNavigate();

  return (
    <>
      {/* Navigation déclarative */}
      <Link to="/consultants" className="text-indigo-600">
        Voir consultants
      </Link>

      {/* Navigation programmatique */}
      <button onClick={() => navigate('/consultants')}>
        Aller aux consultants
      </button>
    </>
  );
}

Performance

Éviter Re-renders Inutiles

typescript
// useMemo pour calculs coûteux
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(data);
}, [data]);

// useCallback pour fonctions stables
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

// React.memo pour composants
const MemoizedComponent = React.memo(ExpensiveComponent);

Lazy Loading

typescript
import { lazy, Suspense } from 'react';

const Calendar = lazy(() => import('./pages/Calendar'));

function App() {
  return (
    <Suspense fallback={<div>Chargement...</div>}>
      <Calendar />
    </Suspense>
  );
}

Accessibilité

tsx
// Labels explicites
<label htmlFor="email" className="block text-sm font-medium">
  Email
</label>
<input id="email" type="email" />

// Boutons avec texte ou aria-label
<button aria-label="Fermer">
  <XIcon />
</button>

// Roles sémantiques
<nav role="navigation">
  <ul role="list">
    <li><Link to="/">Accueil</Link></li>
  </ul>
</nav>

Erreurs Courantes à Éviter

❌ Mauvais

typescript
// Mutation directe du state
consultants.push(newConsultant);
setConsultants(consultants);

// Oubli de key dans liste
{consultants.map(c => <Card consultant={c} />)}

// Effet sans dépendances
useEffect(() => {
  loadData(); // Re-run à chaque render!
});

// Handler inline
<button onClick={() => handleClick(id)}>Click</button> // Re-créé à chaque render

✅ Bon

typescript
// Immutabilité
setConsultants(prev => [...prev, newConsultant]);

// Keys uniques
{consultants.map(c => <Card key={c.id} consultant={c} />)}

// Dépendances correctes
useEffect(() => {
  loadData();
}, [filters]);

// Handler stable
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

Checklist

Avant de commiter du code React:

  • Composants < 300 lignes (extraire si plus grand)
  • Props typées avec TypeScript
  • Keys uniques sur les listes
  • useEffect avec dépendances correctes
  • Pas de state mutation directe
  • Loading/Error states gérés
  • Responsive design (Tailwind responsive classes)
  • Accessibilité de base (labels, aria)
  • Pas de console.log oubliés
  • Tests pour composants critiques

Ressources