Skip to Content

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/reactive est 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/reactive est 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 de useEffect(() => …, [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 service core_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’état visible change → 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 de core_course est la référence vivante.


4. Quand choisir reactive, React ou vanilla ?

CritèreVanilla+core/*core/reactiveReact
Affichage/formulaire simple✗ (trop)✗ (trop)
Éditeur de cours / formatsobligatoire
État client riche, sans bundle
App autonome très richepossible
Poids ajouténulquasi nul (dans le core)~130 Ko
Courbe d’apprentissagefaiblemoyenne (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-live pour 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/modal gè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-live ou 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/Espace pour activer, flèches pour naviguer, Échap pour 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_amd sur 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

Le StateManager (état observable, read-only par défaut), les mutations (seules autorisées à modifier l’état, appellent souvent un web service), les components (vues attachées au DOM), les watchers (abonnements ciblés à des événements d’état). Flux unidirectionnel : l’UI dispatch une mutation → l’état change → les watchers concernés re-rendent leur morceau de DOM.

Q2. Pourquoi Moodle a-t-il créé core/reactive plutôt que d’adopter React ?

Réponse

Pour avoir un modèle d’état réactif (pour l’éditeur de cours) sans imposer React à tout l’écosystème : pas de bundle supplémentaire, pas de dépendance de version, cohérence avec le rendu serveur et les web services. C’est un Flux/Redux minimaliste natif à Moodle.

Q3. Pourquoi l’index de cours reste-t-il synchronisé avec la zone centrale de l’éditeur ?

Réponse

Parce que les deux sont des components reactive abonnés au même état. Une mutation (ex. déplacer/cacher une activité) met à jour l’état central, et tous les composants concernés (section, index) réagissent via leurs watchers — d’où la synchronisation sans rechargement.

Q4. Citez deux règles d’accessibilité JS incontournables pour une UI dynamique.

Réponse

(1) Gestion du focus dans les modales : déplacer le focus dedans à l’ouverture, le piéger, le rendre au déclencheur à la fermeture ; et restaurer le focus après un re-render. (2) ARIA à jour + annonces : 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

Les watchers ciblés : reactive re-rend uniquement le nœud de DOM correspondant à l’événement d’état survenu (ex. un seul item modifié), au lieu de re-rendre toute la liste. Combiné à la délégation d’événements, au chargement JS au plus près de l’usage et au batching des web services, c’est ce qui garde l’UI fluide.

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.