Vues basées sur les rôles, formulaires complexes, tableaux de données
Construire les outils des instructeurs
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Implémenter le RBAC frontend
Rôles, permissions et guards de navigation
2. Vues conditionnelles par rôle
Même page, contenu différent selon l'utilisateur
3. Tableaux de données
Tri, pagination, recherche client et serveur
4. Formulaires multi-step
Champs dynamiques, formulaires imbriqués
5. Patterns d'admin : bulk actions, confirmations, audit log
RBAC frontend : rôles, permissions, guards
Comprendre qui a le droit de faire quoi — et les limites du frontend
Vues conditionnelles par rôle
Même URL, contenu différent — le pattern roleGate
Tableaux de données
Tri, pagination, recherche — client vs serveur
Formulaires complexes
Multi-step, champs dynamiques, formulaires imbriqués
Patterns d'admin
Bulk actions, confirmation dialogs, audit log
Role-Based Access Control
Qui a le droit de voir quoi ? Le RBAC frontend guide l'UX, mais la sécurité est côté serveur.
Rôle
Un groupe de permissions
const roles = {
"admin": ["read", "write", "delete"],
"instructor": ["read", "write"],
"student": ["read"]
};
Permission
Une action spécifique autorisée
const canDelete = (user) =>
user.permissions.includes("delete");
const canEdit = (user) =>
user.permissions.includes("write");
💡 Un rôle est un raccourci — on vérifie toujours la permission, pas le rôle directement.
function usePermissions() {
const { user } = useAuth();
const hasPermission = (perm: string) =>
user?.permissions?.includes(perm) ?? false;
const hasRole = (role: string) =>
user?.role === role;
return { hasPermission, hasRole };
}
// Utilisation
const { hasPermission } = usePermissions();
{hasPermission("delete") && (
<button onClick={handleDelete}>Supprimer</button>
)}
💡 Centraliser la logique dans un hook = un seul endroit à modifier si les règles changent.
// ProtectedRoute — empêche la navigation
function ProtectedRoute({ permission }) {
const { hasPermission } = usePermissions();
if (!hasPermission(permission)) {
return <Navigate to="/unauthorized" />;
}
return <Outlet />;
}
// Dans le router
<Route element={<ProtectedRoute permission="write" />}>
<Route path="/admin/courses" element={<CourseAdmin />} />
</Route>
⚠️ Un guard empêche la navigation, mais ne protège pas les données !
L'API doit aussi vérifier les permissions — le frontend est contournable.
✅ Ce que le RBAC frontend fait
❌ Ce que le RBAC frontend NE fait PAS
Règle d'or : masquer un bouton côté frontend n'est JAMAIS une mesure de sécurité.
Le backend doit TOUJOURS vérifier les permissions.
Même page, contenu différent selon le rôle
Un instructeur voit les outils de gestion, un étudiant voit ses notes.
// Composant qui affiche selon le rôle
function DashboardPage() {
const { hasRole } = usePermissions();
return (
<div>
<StatsCards /> {/* tout le monde */}
{hasRole("instructor") && (
<CourseManagement />
)}
{hasRole("student") && (
<MyGrades />
)}
</div>
);
}
💡 Même URL, composants différents — pas besoin de routes séparées pour chaque rôle.
function IfPermission({ permission, children, fallback }) {
const { hasPermission } = usePermissions();
if (!hasPermission(permission)) return fallback ?? null;
return children;
}
// Utilisation déclarative
<IfPermission permission="delete"
fallback={<span>Lecture seule</span>}>
<button onClick={handleDelete}>Supprimer</button>
</IfPermission>
💡 Le fallback permet d'afficher un message au lieu de rien — meilleure UX.
👨🎓 Étudiant voit
👩🏫 Instructeur voit
💡 Même composant CoursePage, mais les sous-composants changent selon le rôle.
Tri, pagination, recherche
Le composant le plus complexe d'une interface d'administration.
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
sortable?: boolean;
pagination?: { page: number; pageSize: number; total: number };
onSort?: (key: string, dir: "asc" | "desc") => void;
onPageChange?: (page: number) => void;
}
Tri
Cliquer sur l'en-tête pour trier
Pagination
Naviguer page par page
Recherche
Filtrer les résultats
const [sortKey, setSortKey] = useState("name");
const [sortDir, setSortDir] = useState("asc");
const sorted = useMemo(() =>
[...data].sort((a, b) => {
const dir = sortDir === "asc" ? 1 : -1;
return a[sortKey] > b[sortKey] ? dir : -dir;
}),
[data, sortKey, sortDir]
);
💡 useMemo évite de re-trier à chaque render — on ne trie que quand les données ou le critère changent.
✅ Côté client
Pour les petites listes (< 100)
// Tout est en mémoire
const pageData = sorted
.slice(start, end);
✅ Côté serveur
Pour les grandes listes (> 100)
// Appel API avec params
fetch(`/api/courses?page=2&sort=name`)
⚠️ Pagination côté client pour 10 000 éléments = crash du navigateur !
const { data, isLoading } = useQuery({
queryKey: ["courses", page, sortKey, sortDir],
queryFn: () =>
fetch(`/api/courses?page=${page}&sort=${sortKey}&dir=${sortDir}`)
.then(r => r.json())
});
// Réponse du serveur
{
"data": [...], // 20 éléments
"page": 2,
"totalPages": 50,
"total": 1000
}
💡 Le queryKey change → TanStack Query refait l'appel automatiquement. Pas de fetch manuel !
// Debounce : attendre 300ms après la dernière frappe
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const { data } = useQuery({
queryKey: ["courses", debouncedSearch],
queryFn: () =>
fetch(`/api/courses?q=${debouncedSearch}`)
.then(r => r.json())
});
💡 Sans debounce, chaque frappe envoie une requête — "abc" = 3 appels API en 300ms !
Multi-step, dynamiques, imbriqués
Les formulaires d'admin ne sont jamais simples — il faut les structurer.
const [step, setStep] = useState(0);
const methods = useForm({ defaultValues });
const steps = [
{ title: "Infos", component: StepInfos },
{ title: "Détails", component: StepDetails },
{ title: "Confirmation", component: StepConfirm }
];
1️⃣ Infos
Titre, catégorie
2️⃣ Détails
Description, modules
3️⃣ Confirmation
Vérifier et valider
💡 Un seul useForm pour tous les steps — les données sont préservées entre les étapes.
const nextStep = async () => {
const valid = await methods.trigger(stepFields[step]);
if (valid) setStep(s => s + 1);
};
// Champs à valider par étape
const stepFields = [
["title", "category"], // step 0
["description", "modules"], // step 1
];
💡 trigger() de React Hook Form valide uniquement les champs de l'étape courante — pas toute la page.
const { fields, append, remove } = useFieldArray({
control: methods.control,
name: "modules"
});
{fields.map((field, index) => (
<div key={field.id}>
<input {...methods.register(`modules.${index}.name`)} />
<button onClick={() => remove(index)}>✕</button>
</div>
))}
<button onClick={() => append({ name: "" })}>+ Module</button>
💡 useFieldArray gère l'ajout/suppression dynamique — chaque champ a un id unique stable.
// Sous-formulaire réutilisable
function AddressFields({ prefix }) {
const { register } = useFormContext();
return (
<>
<input {...register(`${prefix}.street`)} />
<input {...register(`${prefix}.city`)} />
</>
);
}
// Utilisation
<AddressFields prefix="billing" />
<AddressFields prefix="shipping" />
💡 useFormContext partage le formulaire parent — pas besoin de passer les props en cascade.
Bulk actions, confirmations, audit log
Les actions d'administration nécessitent des précautions particulières.
const [selected, setSelected] = useState<string[]>([]);
const toggleAll = () =>
setSelected(prev =>
prev.length === data.length ? [] : data.map(d => d.id)
);
const bulkDelete = async () => {
await mutateAsync(selected);
setSelected([]);
};
⚠️ Bulk delete SANS confirmation = catastrophe potentielle !
Toujours afficher un dialog de confirmation avec le nombre d'éléments sélectionnés.
const [confirm, setConfirm] = useState(null);
const handleDelete = (id: string) => {
setConfirm({ id, message: "Supprimer ce cours ?" });
};
const confirmAction = async () => {
await deleteCourse(confirm.id);
setConfirm(null);
};
Les confirmations sont OBLIGATOIRES pour :
💡 Afficher le nombre d'éléments affectés dans le dialog : "Supprimer 3 cours ?" est plus clair que "Supprimer ?"
interface AuditEntry {
id: string;
action: "create" | "update" | "delete";
resource: string;
userId: string;
timestamp: Date;
details?: Record<string, unknown>;
}
Pourquoi un audit log ?
💡 L'audit log est côté serveur — le frontend se contente de l'afficher en lecture seule.
❌ Masquer un bouton = sécurité
// Frontend only
{!canDelete && <Btn/>}
// API non protégée ❌
// Frontend + Backend
{!canDelete && <Btn/>}
// API vérifie le token ✓
✅ Le backend doit TOUJOURS vérifier les permissions.
❌ Pagination côté client pour 10 000 éléments
Charger tout en mémoire → navigateur crash
✅ Pagination côté serveur dès que la liste dépasse ~100 éléments.
❌ Multi-step sans sauvegarde intermédiaire
L'utilisateur navigue ailleurs → toutes les données du formulaire perdues
✅ Sauvegarder en brouillon (localStorage ou API) à chaque étape.
🔒 RBAC frontend = UX
La vraie sécurité est côté serveur — le frontend guide l'utilisateur
📊 Pagination serveur
Nécessaire pour les grandes listes (> 100 éléments)
⚠️ Confirmations obligatoires
Pour toute action destructive — suppression, bulk actions
📝 Audit log
Qui a fait quoi et quand — traçabilité et confiance
Après cette leçon, vous savez :
RBAC · Vues conditionnelles · DataTable · Multi-step · Patterns d'admin
Le RBAC frontend est pour l'UX, pas pour la sécurité
Les pages d'admin sont le cœur de l'application instructeur — prenez le temps de les construire correctement.