
Tuto : Créer sa Librairie Réactive en JS
Apprenez à créer une librairie réactive minimaliste en JS avec l'objet Proxy pour comprendre les bases de la réactivité : state, event binding et rerendering.
Dans le développement web moderne, la réactivité est un concept clé assurant une mise à jour automatique et fluide d’une interface. Dans cet article, nous allons explorer comment créer un système de réactivité minimaliste en JavaScript, en utilisant l’objet Proxy
, et comprendre son lien avec les mécanismes utilisés par les frameworks JavaScript Frontend.
Comprendre la réactivité en JavaScript
C’est quoi la réactivité ?
La réactivité en JavaScript est le concept fondamental à la base de frameworks comme Vue.js, React, Angular, et Svelte. Elle contribue à grandement simplifier le développement d’applications web en permettant de lier des données à une interface utilisateur.
Lorsqu’une donnée (on parle aussi « d’état ») change, l’interface (UI) se met automatiquement à jour pour refléter ce changement.
Cela permet de mieux contrôler le flux de données dans une application et évite d’avoir à mettre à jour manuellement le DOM, ce qui peut être fastidieux et source d’erreurs : multiples sélécteurs, gestion des événements, etc.
L’objet Proxy
L’objet Proxy
en JavaScript permet de définir des comportements personnalisés pour les opérations effectuées sur un objet. En utilisant un Proxy
, on peut notamment intercepter et réagir à des modifications d’un objet, comme l’ajout ou la modification de propriétés.
Voici un petit extrait de code JavaScript pour comprendre le rôle du Proxy
:
// Déclaration de l'état initial
const obj = { count: 0 };
// Création d'un proxy pour intercepter les modifications sur l'objet
const proxy = new Proxy(obj, {
set(target, property, value) {
console.log(`Modification de ${property} : ${value}`);
target[property] = value;
return true;
},
});
proxy.count = 1; // Affiche : "Modification de count : 1"
Dans cet exemple, chaque fois que nous modifions la propriété count
, le set
de notre Proxy
est appelé et modifie sa valeur avec target[property] = value
(target
fait référence à l’objet original obj
qui est surveillé par le proxy).
Le Proxy agit ici comme une sorte d’écouteur d’événements, interceptant non pas des interactions mais bien les modifications apportées à l’objet obj
et exécutant une action (ici, afficher un message dans la console) avant de procéder à la modification réelle.
Tutoriel : Créer une librairie réactive minimaliste en JS
Dans ce bref tutoriel, nous allons construire une petite librairie de réactivité en utilisant l’objet Proxy
. Nous allons l’exploiter sur un conteneur HTML et rendre automatiquement les données lorsque l’état change.
Création de l’interface
1. Structure HTML
Avant de plonger dans le code JavaScript, nous aurons besoin d’une interface HTML simple pour afficher notre état.
Pour cela, nous allons créer un système de compteur élémentaire qui incrémente une valeur à chaque fois qu’un bouton est cliqué.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Librairie JS - Réactivité</title>
</head>
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button>Incrémenter</button>
</div>
</body>
</html>
<div id="app">
: C’est le conteneur principal de notre application.<p>Compteur : {{ count }}</p>
: C’est l’endroit où nous afficherons la valeur de notre compteur. La syntaxe{{ count }}
est une expression qui sera remplacée par la valeur réelle decount
<button>Incrémenter</button>
: C’est le bouton sur lequel l’utilisateur cliquera pour incrémenter le compteur.
Lions désormais un script JavaScript à notre fichier HTML afin de gérer la réactivité.
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button>Incrémenter</button>
</div>
<script src="reactivity.js"></script>
</body>
2. Définition du state
Définissons désormais notre state dans un objet JavaScript. Ce sera l’état de notre application, qui contiendra la valeur du compteur.
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button>Incrémenter</button>
</div>
<script src="reactivity.js"></script>
<script>
const state = { count: 0 };
</script>
</body>
Pour rendre ce compteur réactif, nous allons implémenter au sein de notre mini librairie, une fonction reactive()
qui va permettre de lier cet état à notre template HTML.
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button>Incrémenter</button>
</div>
<script src="reactivity.js"></script>
<script>
const state = { count: 0 };
const state = reactive({ count: 0 }, "#app");
</script>
</body>
Cette fonction prend 2 paramètres :
- L’état initial
{ count: 0 }
- Le sélécteur du conteneur réactif (
#app
)
Cette fonction retournera dans state
un objet Proxy
qui interceptera les modifications de l’état et mettra à jour le DOM en conséquence.
3. Binding des événements
Pour rendre notre interface réactive et permettre à l’utilisateur d’interagir avec le compteur, nous devons lier un événement de clic sur le bouton. Lorsque l’utilisateur clique sur le bouton, nous allons incrémenter la valeur du compteur.
Nous utiliserons pour cela un data-attribute data-click
qui indiquera à notre librairie réactive :
- Quoi faire : déclencher la fonction
increment()
- Quand le faire : en cas de clic sur le bouton
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button data-click="increment">Incrémenter</button>
</div>
<script src="reactivity.js"></script>
<script>
const state = reactive({ count: 0 }, "#app");
function increment() {
state.count++;
}
</script>
</body>
La fonction increment()
met ici à jour la valeur de count
par l’intermédiaire de l’objet Proxy state
.
Implémentation de la réactivité
Nous allons maintenant créer la fonction reactive
qui va initialiser le mécanisme de réactivité de notre application à partir de l’état initial et de l’id du conteneur HTML transmis précédemment.
D’abord, on cible en JS le conteneur HTML à partir du sélécteur transmis dans une variable container
et on sauvegarde le template original dans une variable template
.
let container, template;
function reactive(initialState, target) {
container = document.querySelector(target);
template = container.innerHTML;
// 👉 Etape 1 : Binder les événements
// 👉 Etape 2 : Intercepter les modifications de l'état
// 👉 Etape 3 : Mettre à jour le DOM
}
1. Binder les événements
Nous allons créer une fonction bindEvents()
qui va parcourir le conteneur et ajouter des écouteurs d’événements sur les éléments qui ont un attribut data-click
.
let container, template;
function reactive(initialState, target) {
container = document.querySelector(target);
template = container.innerHTML;
bindEvents();
}
function bindEvents() {
container.addEventListener("click", (e) => {
const action = e.target.getAttribute("data-click");
if (action && typeof window[action] === "function") {
window[action]();
}
});
}
En cas de clic à l’intérieur de notre conteneur réactif, on va d’abord vérifier si l’élément cliqué a bien un attribut data-click
. Si c’est le cas, on va récupérer la valeur de cet attribut et exécuter la fonction correspondante via l’objet window
, contenant toutes les fonctions globales de notre page.
2. Intercepter les modifications de l’état
Ensuite, on va créer un Proxy
autour de l’état initial pour intercepter les modifications.
let container, template;
function reactive(initialState, target) {
container = document.querySelector(target);
template = container.innerHTML;
bindEvents();
const state = createProxy(initialState);
}
function bindEvents() { ... }
function createProxy(state) {
return new Proxy(state, {
set(target, property, value) {
target[property] = value;
render(state);
return true;
},
});
}
Chaque fois que l’état est modifié, set
est déclenchée et :
- Propage la modification dans l’objet d’origine avec
target[property] = value
- Met à jour l’interface avec la fonction
render()
, que nous allons créer dans la prochaine étape. On parle de « rerendering »
3. Mettre à jour le DOM
La dernière étape consiste à créer une fonction de rendu render()
, qui va remplacer les expressions {{ ... }}
dans le template par les valeurs correspondantes dans l’état.
Nous allons utiliser la méthode JS native replace()
pour cela.
let container, template;
function reactive(initialState, target) {
container = document.querySelector(target);
template = container.innerHTML;
bindEvents();
const state = createProxy(initialState);
render(state);
return state;
}
function bindEvents() { ... }
function createProxy(state) { ... }
function render(state) {
container.innerHTML = template.replace(
/\{\{\s*([^}]+)\s*\}\}/g,
(_, key) => state[key.trim()] ?? ""
);
}
L’expression régulière /\{\{\s*([^}]+)\s*\}\}/g
permet de capturer tout ce qui se trouve entre {{
et }}
, en ignorant les espaces autour.
Le premier argument _
de la fonction de remplacement (_, key) => state[key.trim()] ?? ""
correspond à la chaîne de caractères complète qui a été trouvée (par exemple, {{ count }}
). On utilise _
par convention pour indiquer que cet argument n’est pas utilisé dans le corps de la fonction.
Le deuxième argument key
correspond au contenu capturé par le groupe ([^}]+)
de l’expression régulière (par exemple, count
).
Cette fonction va donc :
- Extraire la clé entre
{{
et}}
(danskey
) - Utiliser
key.trim()
pour supprimer les espaces superflus autour de la clé - Chercher la valeur correspondante dans l’état et la remplacer dans le template. Si la clé n’existe pas dans l’état, elle renvoie une chaîne vide.
Code final
Et voilà, nous avons créé une librairie réactive minimaliste en JavaScript ! Je vous glisse ci-dessous le code final de notre application, qui se compose de deux fichiers : index.html
et reactivity.js
. 👇
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Librairie JS - Réactivité</title>
</head>
<body>
<div id="app">
<p>Compteur : {{ count }}</p>
<button data-click="increment">Incrémenter</button>
</div>
<script src="reactivity.js"></script>
<script>
const state = reactive({ count: 0 }, "#app");
function increment() {
state.count++;
}
</script>
</body>
</html>
let container, template;
function reactive(initialState, target) {
container = document.querySelector(target);
template = container.innerHTML;
bindEvents();
const state = createProxy(initialState);
render(state);
return state;
}
function bindEvents() {
container.addEventListener("click", (e) => {
const action = e.target.getAttribute("data-click");
if (action && typeof window[action] === "function") {
window[action]();
}
});
}
function createProxy(state) {
return new Proxy(state, {
set(target, property, value) {
target[property] = value;
render(state);
return true;
},
});
}
function render(state) {
container.innerHTML = template.replace(
/\{\{\s*([^}]+)\s*\}\}/g,
(_, key) => state[key.trim()] ?? ""
);
}
Dans ce tutoriel, nous avons créé un système réactif minimaliste en JavaScript en utilisant l’objet Proxy
. Si ce n’est pas dejà le cas, je vous encourage à prendre en main un framework réactif commee Vue.js, proposant un système de réactivité bien plus avancé et optimisé pour des applications complexes (directives, composants, deeply nested…).