Standards React et frontend. Use when "react component", "frontend", "ui", "hook", "jsx".
Définir les standards React pour le frontend du projet consultant-manager.
// 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>
);
}
PascalCase.tsx (ex: ConsultantForm.tsx)useCamelCase.ts (ex: useConsultants.ts)camelCase.ts (ex: formatDate.ts)PascalCase.tsx (ex: Dashboard.tsx)// 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 = () => {};
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();
// ...
}
// 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>;
}
// Pour état simple, local à un composant
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState<FormData>({});
// 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]);
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>
);
}
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>
);
}
sm:, md:, lg:)dark: (si applicable)Boutons:
// 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:
<div className="bg-white shadow rounded-lg p-6">
{/* Contenu */}
</div>
Forms:
<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:
<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>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* 1 col mobile, 2 tablet, 4 desktop */}
</div>
// 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>
);
}
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>
</>
);
}
// 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);
import { lazy, Suspense } from 'react';
const Calendar = lazy(() => import('./pages/Calendar'));
function App() {
return (
<Suspense fallback={<div>Chargement...</div>}>
<Calendar />
</Suspense>
);
}
// 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>
// 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
// 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]);
Avant de commiter du code React: