Chapitre 7.6 — core/reactive et UI avancée
Partie 7 : Frontend — Chapitre 6/6 Public : dev React/Next.js. On a vu vanilla+
core/*(7.3) et React (7.4). Voici la troisième voie : la bibliothèque réactive interne de Moodle, plus l’accessibilité et la performance frontend. Version de référence : Moodle 5.2 (core/reactive, moteur de l’éditeur de cours).
⏱️ TL;DR
core/reactiveest le mini state-manager maison de Moodle : un StateManager (état observable), des mutations (seules autorisées à modifier l’état), des components (vues qui s’abonnent à des parties de l’état via des watchers).- Flux unidirectionnel : l’UI dispatch une mutation → la mutation modifie l’état → les watchers concernés réagissent et re-rendent leur morceau de DOM. Très proche de Redux/Flux.
- C’est le moteur de l’éditeur de cours (drag & drop des sections/activités, index de cours, actions AJAX). Si vous touchez à l’éditeur de cours, vous devez le connaître.
- Quand l’utiliser : UI riche et interactive intégrée au core (surtout l’éditeur de cours) ou quand vous voulez de la réactivité sans importer React. Quand ne pas : affichage simple (Mustache), ou app autonome où React est déjà justifié.
- Le drag & drop du core (
core/local/reactive/dragdrop) est fourni ; les key events et la gestion du focus sont cadrées par des helpers d’accessibilité.- Accessibilité JS (transverse) : ARIA correct, gestion du focus dans les modales (piège focus, retour au déclencheur), navigation clavier — non optionnel (Partie 11, Brickfield).
- Performance frontend : limiter les re-rendus (watchers ciblés), déléguer les événements, ne charger le JS que nécessaire, mesurer (Core Web Vitals).
🎯 Objectifs
- Comprendre l’architecture StateManager / mutations / components / watchers de
core/reactive. - Savoir quand choisir reactive plutôt que vanilla ou React.
- Situer reactive dans l’éditeur de cours et son drag & drop.
- Appliquer les règles d’accessibilité JS (ARIA, focus, clavier).
- Connaître les leviers de performance frontend sur Moodle.
1. Pourquoi Moodle a écrit son propre « Redux »
Vers Moodle 4.0, l’équipe a refondu l’éditeur de cours en une UI réactive (drag & drop fluide, index de cours synchronisé, actions AJAX sans rechargement). Elle avait besoin d’un modèle d’état réactif, mais ne voulait pas imposer React (ni sa taille, ni sa version, ni son paradigme) à tout l’écosystème de plugins. D’où core/reactive : une bibliothèque minuscule, sans dépendance, qui apporte le pattern Flux/Redux à du JS vanilla intégré au rendu serveur.
Résultat : une réactivité native à Moodle, disponible partout, cohérente avec les templates Mustache et les web services, et sans bundle supplémentaire.
💡 Pour un dev React :
core/reactiveest un Redux/Zustand minimaliste couplé à un système de composants « à la main » (pas de VDOM). L’état est central et observable ; on ne le mute que par des mutations (≈ vos reducers/actions) ; les vues s’abonnent à des morceaux d’état (≈useSelector) via des watchers et re-rendent elles-mêmes leur DOM (pas de diff automatique). Vous retrouverez le flux unidirectionnel, sans la couche de rendu déclaratif.
2. L’architecture : quatre pièces
2.1 Le StateManager — l’état observable
L’état est un objet structuré (souvent : une liste d’objets + un objet « course »). Le StateManager le rend observable : toute modification émet des événements précis (<element>:created, :updated, :deleted, et par champ <element>.<field>:updated).
2.2 Les mutations — la seule voie de modification
On ne modifie jamais l’état directement. On dispatch une mutation : une fonction asynchrone qui a le droit d’écrire dans l’état (via un stateManager en mode écriture) et qui, typiquement, appelle un web service puis met l’état à jour.
// exemple de mutation (simplifié)
const mutations = {
async toggleItem(stateManager, itemId) {
// 1. appel serveur (source de vérité)
const updated = await Ajax.call([{
methodname: 'mod_simplecheck_toggle_item',
args: {itemid: itemId},
}])[0];
// 2. écriture dans l'état (déclenche les watchers)
stateManager.setReadOnly(false);
const item = stateManager.state.items.get(itemId);
item.checked = updated.checked;
stateManager.setReadOnly(true);
},
};L’état est read-only par défaut ; seules les mutations le passent temporairement en écriture. C’est ce qui garantit que toute modification passe par un point contrôlé (comme un reducer).
2.3 Les components — les vues
Un component reactive est une classe (étendant core/local/reactive/basecomponent) attachée à un élément du DOM. Il déclare :
getWatchers(): la liste des événements d’état qui l’intéressent, chacun lié à une méthode de rendu.- Des méthodes de rendu (
_updateItem,_removeItem…) qui modifient leur DOM en réaction. stateReady(): initialisation quand l’état est prêt.
import {BaseComponent} from 'core/reactive';
export default class extends BaseComponent {
create() {
this.name = 'simplecheck-list';
this.selectors = {ITEM: `[data-for='item']`};
}
getWatchers() {
return [
{watch: `items.checked:updated`, handler: this._refreshItem},
{watch: `items:deleted`, handler: this._removeItem},
];
}
_refreshItem({element}) {
const node = this.getElement(this.selectors.ITEM, element.id);
node?.classList.toggle('checked', element.checked);
}
}2.4 Les watchers — l’abonnement ciblé
Un watcher lie un motif d’événement d’état (items.checked:updated) à un handler. Quand la mutation modifie checked d’un item, le StateManager émet l’événement, et seul le handler concerné s’exécute. C’est la clé de la performance : on ne re-rend pas toute la liste, seulement l’item touché.
💡 Pour un dev React :
getWatchers()= un ensemble deuseEffect(() => …, [state.slice])explicites et granulaires. Là où React diffe un VDOM entier, reactive vous demande d’écrire quel DOM change pour quel événement. Plus de contrôle, plus de code manuel, zéro overhead de VDOM. C’est un compromis « performance/contrôle vs ergonomie ».
3. reactive en action : l’éditeur de cours
L’éditeur de cours (core_course) est l’ implémentation de référence. Son état contient les sections, les course modules (cm) et le course. Quand vous, enseignant :
- glissez une activité d’une section à l’autre → un component drag&drop dispatch une mutation
cmMove→ la mutation appelle le web servicecore_course_edit_module(ou équivalent) → l’état est mis à jour → tous les composants abonnés (la section source, la section cible, l’index de cours) réagissent et se re-rendent de façon synchronisée. - cachez une activité → mutation
cmState→ l’étatvisiblechange → l’activité et son entrée dans l’index de cours se grisent ensemble, sans rechargement.
C’est pourquoi l’index de cours (le tiroir gauche, ch. 1.3) reste toujours synchronisé avec la zone centrale : les deux sont des components abonnés au même état.
⚠️ Piège : si vous développez un format de cours (ch. 6.6) ou surchargez l’éditeur, vos composants doivent s’intégrer à cet état reactive, pas réinventer le leur. Hériter de
format_topics(ch. 6.6) vous branche automatiquement dessus. Introduire un React parallèle dans l’éditeur de cours créerait deux sources de vérité désynchronisées — à proscrire.
📚 Aller plus loin : la doc « Reactive components » est sur moodledev.io/docs/5.2/guides/javascript/reactive . Le code de
course/format/amd/src/local/(composants de l’éditeur) et decore_courseest la référence vivante.
4. Quand choisir reactive, React ou vanilla ?
| Critère | Vanilla+core/* | core/reactive | React |
|---|---|---|---|
| Affichage/formulaire simple | ✅ | ✗ (trop) | ✗ (trop) |
| Éditeur de cours / formats | — | ✅ obligatoire | ✗ |
| État client riche, sans bundle | — | ✅ | — |
| App autonome très riche | — | possible | ✅ |
| Poids ajouté | nul | quasi nul (dans le core) | ~130 Ko |
| Courbe d’apprentissage | faible | moyenne (API maison) | faible (si vous savez React) |
Règle : reactive gagne dès qu’il faut de la réactivité intégrée à Moodle sans le coût de React ; il est obligatoire pour l’éditeur de cours ; React reste pertinent pour une app isolée très riche (ch. 7.4).
5. Drag & drop et key events
Le core fournit un helper core/local/reactive/dragdrop : un mixin pour vos components qui gère le HTML5 drag & drop accessible (annonces, feedback visuel, zones de dépôt). Vous fournissez les callbacks (dragStart, drop…), il gère la mécanique. Ne réimplémentez pas un drag & drop maison : le drag natif est un gouffre d’edge cases (accessibilité clavier, tactile, annulation) que le helper couvre déjà.
Pour les key events (raccourcis, navigation clavier), Moodle privilégie la délégation (ch. 7.3) et des constantes de touches. Toute action réalisable à la souris (déplacer, cocher, ouvrir un menu) doit l’être au clavier — sinon l’UI n’est pas accessible (§6).
6. Accessibilité JavaScript (non négociable)
Une UI dynamique casse l’accessibilité si elle est mal faite. Les règles incontournables (approfondies en Partie 8 et 11 avec le toolkit Brickfield) :
- ARIA correct et à jour :
role,aria-expanded,aria-checked,aria-livepour les régions qui changent. Un état visuel (couleur, barre) doit avoir son équivalent ARIA pour le lecteur d’écran (ex.role="progressbar"+aria-valuenow, ch. 6.4). - Gestion du focus dans les modales (piège classique) : à l’ouverture, déplacer le focus dans la modale ; piéger le focus à l’intérieur (Tab ne sort pas) ; à la fermeture, rendre le focus au déclencheur. Les modules
core/modalgèrent l’essentiel — mais si vous montez du React/reactive dans une modale, vérifiez que le focus est correct. - Annonces dynamiques : quand un contenu apparaît/change sans rechargement (toast, item ajouté), utilisez
aria-liveou les helpers d’annonce du core pour que ce soit perçu par les technologies d’assistance. - Clavier partout : chaque interaction souris a son équivalent clavier (
Enter/Espacepour activer, flèches pour naviguer,Échappour fermer).
⚠️ Piège du focus perdu : après un re-render (reactive/React/
core/templates), l’élément qui avait le focus peut être détruit → le focus « saute » en haut de page, désorientant l’utilisateur clavier/lecteur d’écran. Après un re-render, restaurez le focus sur l’élément équivalent (par id/data-*stable). C’est l’un des bugs d’accessibilité les plus fréquents des UI dynamiques.
💡 Pour un dev React : les exigences sont identiques à celles de vos apps React accessibles (focus management, ARIA, live regions), sauf que Moodle vous tient pour responsable : le plugin directory et l’audit Brickfield (Partie 11) vérifient l’accessibilité, et un LMS a une obligation légale d’accessibilité (secteur éducatif). Ce n’est pas un « nice to have ».
7. Performance frontend
Quelques leviers, du plus impactant au plus fin :
- Ne charger le JS que si nécessaire :
js_call_amdsur la page concernée, pas globalement. Un module chargé partout ralentit tout. - Watchers/handlers ciblés (reactive) : re-rendre le nœud qui change, pas toute la liste. C’est l’avantage structurel de reactive sur un re-render global.
- Délégation d’événements (ch. 7.3) : un écouteur sur un ancêtre plutôt que N écouteurs.
- Batching des web services :
Ajax.call([req1, req2])en un aller-retour (ch. 7.3). - Éviter les bundles lourds : React embarqué = ~130 Ko ; multiplié par plusieurs plugins, l’impact sur le LCP/TBT (Core Web Vitals) devient réel. Préférer reactive quand c’est possible.
- Mesurer : outils navigateur (Performance, Lighthouse), et côté serveur
perfdebug/tool_excimer(Partie 11) — mesurer avant d’optimiser.
⚠️ Piège : un plugin qui charge son JS (surtout un bundle React) sur toutes les pages via un hook global dégrade les Core Web Vitals de tout le site, pas seulement de sa page. Chargez toujours au plus près de l’usage.
8. Synthèse de la Partie 7
Le fil rouge de toute la partie : Moodle est server-first, et le JS y ajoute de l’interactivité par îlots. Vous disposez de trois niveaux — vanilla+core/* (le défaut), core/reactive (réactivité intégrée, obligatoire pour l’éditeur de cours), React (app isolée riche) — et la compétence clé n’est pas d’écrire du React, mais de choisir le bon niveau pour chaque interface. Le plus souvent, le niveau juste est plus bas que votre réflexe de dev SPA ne le suggère.
✏️ Exercices
Exercice 1 — Modifier l’état directement.
Un développeur écrit, dans un handler de clic, stateManager.state.items.get(3).checked = true; directement. Pourquoi est-ce faux, et quelle est la bonne voie ?
✅ Solution
L’état est read-only par défaut et ne doit être modifié que par une mutation : l’écriture directe est soit bloquée, soit contourne le point de contrôle qui garantit que le serveur est mis à jour et que les watchers sont notifiés correctement. Bonne voie : dispatcher une mutation (reactive.dispatch('toggleItem', 3)) qui (1) appelle le web service (source de vérité), (2) passe l’état en écriture, le modifie, et le repasse en read-only — déclenchant les watchers. C’est l’équivalent Moodle de « ne jamais muter le state Redux hors d’un reducer ».
Exercice 2 — Reactive, React ou vanilla ? Choisissez pour : (a) griser une activité et son entrée d’index quand l’enseignant la cache ; (b) afficher une liste statique de badges ; (c) un tableau de bord analytique très interactif, page autonome ; (d) un formulaire d’ajout en modale.
✅ Solution
(a) core/reactive — c’est l’éditeur de cours, l’état partagé synchronise activité + index (obligatoire d’y rester) ; (b) vanilla + Mustache — aucun état ; (c) React (app isolée riche) ou reactive selon l’écosystème, React défendable ici ; (d) vanilla + core/modal_form — pas besoin de framework. Règle : reactive pour l’éditeur, React pour l’app isolée riche, vanilla pour le reste.
Exercice 3 — Index désynchronisé. Vous surchargez l’éditeur de cours avec un composant React qui gère son propre état des sections. Résultat : l’index de cours n’est plus synchronisé avec vos changements. Diagnostic ?
✅ Solution
Vous avez créé une seconde source de vérité (l’état React) à côté de l’état core/reactive de l’éditeur de cours. L’index de cours est un component abonné à l’état reactive ; vos changements React ne le notifient pas → désynchronisation. Correctif : ne pas introduire React dans l’éditeur ; s’intégrer à l’état reactive existant (composants reactive + mutations), en héritant de format_topics pour être branché automatiquement. Une seule source de vérité pour l’éditeur.
Exercice 4 — Focus perdu après re-render. Après un re-render reactive de la liste, l’utilisateur au clavier voit le focus « sauter » en haut de page. Cause et correctif ?
✅ Solution
Le re-render a détruit le nœud qui avait le focus (ou son conteneur), et le navigateur a replacé le focus par défaut. Correctif : mémoriser l’élément focalisé (par un id/data-* stable) avant le re-render, puis restaurer le focus sur l’élément équivalent après. C’est une exigence d’accessibilité des UI dynamiques (valable aussi pour React et core/templates). Idéalement, ne re-render que le strict nœud modifié (watcher ciblé) pour ne pas toucher l’élément focalisé.
Exercice 5 — JS partout. Un plugin charge son bundle React via un hook global exécuté sur chaque page. Impact et correctif ?
✅ Solution
Impact : le bundle (~130 Ko) est chargé et parsé sur toutes les pages, y compris celles qui n’utilisent pas le composant → dégradation des Core Web Vitals (LCP, TBT) de tout le site. Correctif : charger le module au plus près de l’usage — $PAGE->requires->js_call_amd(...) uniquement sur la page/région concernée, ou via le {{#js}} du template du composant. Ne jamais charger du JS lourd globalement.
🧠 Quiz de révision
Q1. Quelles sont les quatre pièces de core/reactive et le sens du flux ?
Réponse
Q2. Pourquoi Moodle a-t-il créé core/reactive plutôt que d’adopter React ?
Réponse
Q3. Pourquoi l’index de cours reste-t-il synchronisé avec la zone centrale de l’éditeur ?
Réponse
Q4. Citez deux règles d’accessibilité JS incontournables pour une UI dynamique.
Réponse
role/aria-* corrects et aria-live pour les changements sans rechargement, chaque interaction souris ayant son équivalent clavier.Q5. Quel est le principal levier de performance de reactive par rapport à un re-render global ?
Réponse
Fin de la Partie 7. Vous maîtrisez le frontend de Moodle : le HTML logic-less (Mustache), l’animation par îlots (core/*), l’intégration de React et de Tailwind scopés, et la voie réactive interne (core/reactive). Surtout, vous savez choisir le bon niveau — la compétence qui distingue un dev qui « colle du React partout » d’un dev qui construit des interfaces Moodle durables.
Partie suivante : Partie 8 — Les thèmes Moodle. On répond à LA question du dev front qui arrive sur Moodle : « est-on vraiment flexible pour créer un thème ? ». Réponse : oui, radicalement — via SCSS, variables Bootstrap, surcharges de templates et de renderers — avec une structure et un coût précis. On construit theme_maboite de bout en bout.