Composants et pages
Documentation React
Les composants sont les éléments de base d'un projet React. Chacun représente une partie de l'interface utilisateur, réutilisable et maintenable.
Organisation des composants
Structure recommandée
src/components/
├── app/ # Composants d'infrastructure
│ ├── Provider/
│ │ └── index.jsx
│ └── Router/
│ ├── index.jsx
│ └── Guards/
│ └── index.jsx
├── pages/ # Pages de l'application
│ ├── public/ # Pages sans authentification
│ │ ├── LoginPage/
│ │ └── WelcomePage/
│ ├── private/ # Pages authentifiées
│ │ ├── HomePage/
│ │ └── SettingsPage/
│ └── errors/ # Pages d'erreur
│ └── Error404Page/
├── forms/ # Composants de formulaire
│ ├── LoginForm/
│ └── ItemForm/
├── ui/ # Composants UI réutilisables
│ ├── Card/
│ ├── Modal/
│ └── Header/
└── layouts/ # Layouts de pages
├── MainLayout/
└── AuthLayout/
Convention de nommage
- Dossier par composant : Chaque composant dans son propre dossier
- index.jsx : Fichier principal du composant
- PascalCase : Noms de composants en PascalCase
src/components/ui/Card/ ├── index.jsx # Composant principal ├── Card.module.css # Styles (optionnel) └── Card.test.jsx # Tests (optionnel)
Créer un composant
Composant simple
// src/components/ui/Card/index.jsx
export const Card = ({ children, className = '' }) => {
return (
<div className={`bg-white rounded-lg shadow-md p-4 ${className}`}>
{children}
</div>
);
};
Composant avec props
// src/components/ui/Button/index.jsx
export const Button = ({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
onClick,
type = 'button',
className = '',
}) => {
const variants = {
primary: 'bg-primary text-white hover:bg-primary-dark',
secondary: 'bg-secondary text-white hover:bg-secondary-dark',
outline: 'border-2 border-primary text-primary hover:bg-primary/10',
ghost: 'text-primary hover:bg-primary/10',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={`
inline-flex items-center justify-center
rounded-md font-medium transition-all
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]}
${sizes[size]}
${className}
`}
>
{loading && (
<span className="mr-2 animate-spin">⟳</span>
)}
{children}
</button>
);
};
Composant Input personnalisé
// src/components/form/Input/index.jsx
export const Input = (props) => {
const { label, id, error, ...inputProps } = props;
return (
<div className="flex flex-col gap-2">
{label && (
<label
htmlFor={id}
className="text-sm font-medium text-gray-700"
>
{label}
</label>
)}
<input
id={id}
className={`
bg-gray-100 p-4 rounded-lg
outline-none focus:ring-2 focus:ring-primary
${error ? 'ring-2 ring-red-500' : ''}
`}
{...inputProps}
/>
{error && (
<span className="text-sm text-red-500">{error}</span>
)}
</div>
);
};
Structure d'une page
Page simple
// src/components/pages/private/HomePage/index.jsx
import { useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
export const HomePage = () => {
const navigate = useNavigation();
const [session] = useGlobalStates('session');
return (
<div className="min-h-screen bg-gray-100">
{/* Header */}
<header className="bg-white shadow p-4">
<h1 className="text-xl font-bold">Accueil</h1>
</header>
{/* Content */}
<main className="p-4">
<p>Bienvenue {session?.user?.name}</p>
</main>
{/* Navigation */}
<nav className="fixed bottom-0 left-0 right-0 bg-white shadow-lg">
<div className="flex justify-around p-2">
<button onClick={() => navigate('/')}>Accueil</button>
<button onClick={() => navigate('/settings')}>Paramètres</button>
</div>
</nav>
</div>
);
};
Page avec chargement de données
// src/components/pages/private/ItemsPage/index.jsx
import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect, useState } from 'react';
import { Card } from '../../ui/Card';
export const ItemsPage = () => {
const api = useApi();
const navigate = useNavigation();
const [items, setItems] = useGlobalStates('items');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchItems = async () => {
setLoading(true);
setError(null);
const response = await api.private.get('items');
if (response.success) {
setItems(response.data);
} else {
setError(response.error);
}
setLoading(false);
};
fetchItems();
}, []);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<span className="animate-spin text-4xl">⟳</span>
</div>
);
}
if (error) {
return (
<div className="p-4 text-center text-red-500">
Erreur: {error}
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 p-4">
<h1 className="text-2xl font-bold mb-4">Mes items</h1>
<div className="space-y-4">
{items.map((item) => (
<Card
key={item.id}
onClick={() => navigate(`/items/${item.id}`)}
className="cursor-pointer hover:shadow-lg transition-shadow"
>
<h2 className="font-semibold">{item.label}</h2>
<p className="text-gray-600 text-sm">{item.description}</p>
</Card>
))}
</div>
</div>
);
};
Utiliser les layouts
Créer un layout
// src/components/layouts/MainLayout/index.jsx
import { useNavigation } from '@cap-rel/smartcommon';
export const MainLayout = ({ children, title, showBack = true }) => {
const navigate = useNavigation();
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
{/* Header */}
<header className="bg-white shadow p-4 flex items-center gap-4">
{showBack && (
<button onClick={() => navigate(-1)} className="text-primary">
← Retour
</button>
)}
<h1 className="text-xl font-bold">{title}</h1>
</header>
{/* Content */}
<main className="flex-1 p-4">
{children}
</main>
{/* Bottom navigation */}
<nav className="bg-white shadow-lg p-2">
<div className="flex justify-around">
<button onClick={() => navigate('/')}>🏠</button>
<button onClick={() => navigate('/search')}>🔍</button>
<button onClick={() => navigate('/profile')}>👤</button>
</div>
</nav>
</div>
);
};
Utiliser un layout
// src/components/pages/private/SettingsPage/index.jsx
import { MainLayout } from '../../layouts/MainLayout';
import { useGlobalStates, useIntl } from '@cap-rel/smartcommon';
export const SettingsPage = () => {
const { t, lng, setLng } = useIntl();
const [theme, setTheme] = useGlobalStates('settings.theme');
return (
<MainLayout title={t('settings.title')}>
<div className="space-y-6">
{/* Langue */}
<section className="bg-white rounded-lg p-4">
<h2 className="font-semibold mb-2">{t('settings.language')}</h2>
<select
value={lng}
onChange={(e) => setLng(e.target.value)}
className="w-full p-2 border rounded"
>
<option value="fr">Français</option>
<option value="en">English</option>
</select>
</section>
{/* Thème */}
<section className="bg-white rounded-lg p-4">
<h2 className="font-semibold mb-2">{t('settings.theme')}</h2>
<div className="flex gap-4">
<button
onClick={() => setTheme('light')}
className={`p-4 rounded ${theme === 'light' ? 'ring-2 ring-primary' : ''}`}
>
☀️ Clair
</button>
<button
onClick={() => setTheme('dark')}
className={`p-4 rounded ${theme === 'dark' ? 'ring-2 ring-primary' : ''}`}
>
🌙 Sombre
</button>
</div>
</section>
</div>
</MainLayout>
);
};
Patterns courants
Formulaire avec validation Zod
import { useForm, useApi, useNavigation } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2, "Minimum 2 caractères"),
email: z.string().email("Email invalide"),
});
const ContactForm = () => {
const api = useApi();
const navigate = useNavigation();
const form = useForm({ schema });
const handleSubmit = async (data) => {
const response = await api.private.post('contacts', { json: data });
if (response.success) {
navigate('/contacts');
}
};
return (
<Form form={form} onSubmit={handleSubmit} className="space-y-4">
<Input name="name" label="Nom" />
<Input name="email" type="email" label="Email" />
<Button type="submit" loading={form.formState.isSubmitting}>
Envoyer
</Button>
</Form>
);
};
Modal/Drawer avec animation
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const Modal = ({ isOpen, onClose, children }) => {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Content */}
<motion.div
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
className="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 z-50"
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
};
Liste avec état vide
const ItemsList = ({ items }) => {
if (items.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p className="text-4xl mb-4">📭</p>
<p>Aucun élément pour le moment</p>
</div>
);
}
return (
<div className="space-y-4">
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
</div>
);
};
Utiliser SmartCommon
Préférez les composants SmartCommon aux composants customs quand c'est possible :
import {
Form,
Input,
Select,
Checkbox,
Button,
Card,
Modal,
} from '@cap-rel/smartcommon';
Voir SmartCommon pour la liste complète.
Voir aussi
- SmartCommon - Composants prêts à l'emploi
- Hooks - Hooks disponibles
- Routage - Navigation entre pages
- Animations - Transitions de pages