Skip to content

Donnée réactive

L’interaction avec l’utilisateur est l’élément principal à l’origine de la conception des frameworks JavaScript modernes. Dans ce chapitre, nous verrons comment le framework nous permet de définir des données réactives, un concept central dans leur fonctionnement, facilitant grandement la gestion des changements d’état au sein d’une application frontend.

Notion d'immuabilité

Types immuables

En JavaScript, les variables de type primitif (String, Number, Boolean, etc.) sont dites immuables (immutables). Cela signifie que si leur valeur est transmise à une autre variable, un attribut ou un argument de fonction, la valeur sera copiée en mémoire:

js
let a = 1;
const b = a;
a = 2;
console.log(a); // => 2
console.log(b); // => 1

Dans cet exemple, la valeur de la variable a est copiée dans l’emplacement mémoire associé à la variable b. Les deux valeurs sont donc indépendantes et peuvent être modifiées indépendamment l’une de l’autre.

Ces valeurs sont dites immuables, car chaque modification de celles-ci entraîne leur réécriture dans l’espace mémoire de la machine.

Types muables

À l’inverse, les variables contenant des valeurs de type objet (Object, Array, Function) fonctionnent de façon muables (mutable) :

js
const a = [1, 2, 3];
const b = a;
a[1] = 0;
console.log(a); // [1, 0, 3]
console.log(b); // [1, 0, 3]

Avec un tableau, cette fois, la modification de la variable a altère également la valeur de b, puisque les deux variables font référence au même emplacement dans l’espace mémoire. La valeur est dite muable, car il est possible de l’éditer sans que cela provoque sa réécriture complète. Les variables a et b font donc référence à la même valeur.

NOTE

📌 Une autre façon de voir les choses est de considérer que, dans l’opération b = a, c’est la référence stockée par a vers une valeur en mémoire qui est copiée. Cette référence est, elle, immuable : b peut ensuite être modifiée pour pointer vers une autre valeur en mémoire, sans affecter a.

Cette différence dans le traitement des données correspond aux usages algorithmiques les plus courants pour chaque type de données : on souhaitera généralement copier les valeurs les plus simples, mais conserver une référence pour les structures de données plus complexes et volumineuses, comme les objets et les tableaux.

Edition du DOM

Dans le cas où l’on souhaite éditer l’état d’une application Web, chaque changement d’état est généralement représenté au niveau d’une variable JavaScript, puis le changement est répercuté au niveau du DOM. Prenons, par exemple, le cas d’un compteur que l’on incrémente à l’aide d’un bouton :

html
<p id="counter">0</p>
<button id="increment-button">Add</button>
js
let counterValue = 0;
const incrementButton = document.getElementById("increment-button");
const counterElement = document.getElementById("counter");

incrementButton.addEventListener("click", () => {
  counterValue++;
  counterElement.textContent = counterValue;
});

Chaque clic sur le bouton incrémente la valeur affichée par l’application.

Une dualité apparaît ici entre l’état de counterValue et celui de l’attribut textContent, qui copie la valeur de la variable. Nous retrouvons ici un comportement d’immuabilité, avec des états indépendants pour deux variables que l’on cherche toutefois à synchroniser.

Le maintien de cette synchronisation d’état peut paraître relativement trivial à assurer dans ce cas, mais il peut facilement se complexifier si l’on souhaite afficher une même valeur à différents endroits de l’application ou si différents facteurs peuvent modifier cette valeur. Imaginons, par exemple, qu’il soit possible de doubler ou de réinitialiser la valeur du compteur à l’aide d’autres boutons.

Dans de tels scénarios, il serait préférable que cette valeur se comporte plutôt comme une donnée muable. Nous pourrions ainsi y faire référence à plusieurs endroits, tout en maintenant un état cohérent de la donnée à travers ces différents usages.

C’est ce type de comportement par référence que les frameworks JavaScript mettent en place à l’aide d’un moteur de réactivité.

Définir une donnée réactive

Si l’on cherche à implémenter naïvement un compteur en utilisant un framework:

jsx
export default function App() {
  let counter = 0;
  const increment = () => {
    counter++;
    console.log(counter);
  };

  return (
    <div>
      <p>{counter}</p>
      <button onClick={increment}>Add</button>
    </div>
  );
}

On constate que la valeur affichée reste à 0. Pourtant, le console.log atteste que la valeur de counter évolue bien dans le temps. Toutefois, le changement n’est pas automatiquement répercuté au niveau du DOM.

Pour permettre au DOM de se référer dynamiquement à la valeur du compteur, il faut utiliser le système de réactivité du framework :

jsx
import { useState } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);

  const increment = () => {
    setCounter((counter) => counter + 1);
    console.log(counter);
  };

  return (
    <div>
      <p>{counter}</p>
      <button onClick={increment}>Add</button>
    </div>
  );
}

La fonction useState fournie par la librairie React prend en argument la valeur initiale de la variable counter et retourne deux valeurs : counter, qui permet de lire la valeur de l’état, et setCounter, qui permet de modifier cet état. En dissociant la lecture de l’écriture, React est en mesure d’identifier plus facilement lorsque l’état de la donnée est modifié et il répercute automatiquement ce changement sur l’ensemble des endroits où la donnée est utilisée.

WARNING

⚠️ Si vous testez ce code, vous remarquerez que le console.log n’affiche pas la valeur de counter en prenant en compte le dernier incrément effectué à la ligne précédente. Cela s’explique car React.js n’exécute pas immédiatement la fonction d’incrémentation. Il attend le prochain rafraîchissement/rendu de l’interface pour appliquer les modifications d’état. Plus de détails plus tard dans le chapitre Cyble de vie.

Grâce à ce système, à chaque modification de la valeur de counter, le framework mettra automatiquement à jour tous les endroits où la valeur est affichée, comme si chaque usage utilisait cette valeur par référence.

NOTE

📌 Le détail de la façon dont le framework gère la mise à jour de la donnée peut être un peu plus complexe à appréhender, et il n’est pas nécessaire de le comprendre pour commencer à utiliser des variables réactives. C’est pourquoi cela sera abordé dans des chapitres plus avancés.

Condition et itération réactives

Cette réactivité simplifie tout particulièrement l’interaction avec des conditions en HTML :

jsx
import { useState } from "react";

export default function App() {
  const [isVisible, setIsVisible] = useState(false);
  const toggleVisible = () => setIsVisible((isVisible) => !isVisible);

  return (
    <div>
      <button onClick={toggleVisible}>{isVisible ? "Hide" : "Show"}</button>
      {isVisible ? <p>I'm visible !</p> : null}
    </div>
  );
}

Dans cet exemple, la variable réactive conditionne l’affichage d’une partie du composant. Lorsque la variable change d’état, la condition est automatiquement réévaluée et les éléments du DOM sont modifiés en conséquence. En cliquant sur le bouton, il est possible ici d'afficher ou de faire disparaître dynamiquement le paragraphe "I'm visible".

La réactivité est encore plus intéressante dans le cas d’une itération :

jsx
import { useState } from "react";

export default function App() {
  const [fruits, setFruits] = useState(
    ["Banana", "Orange", "Kiwi", "Apple"]
    );
  const shuffleFruits = () =>
    setFruits((fruits) => [fruits[2], fruits[0], fruits[3], fruits[1]]);

  return (
    <div>
      <ul>
        {fruits.map((fruit) => (
          <li key={fruit}>{fruit}</li>
        ))}
      </ul>
      <button onClick={shuffleFruits}>Shuffle</button>
    </div>
  );
}

En cliquant sur le bouton, l’ordre des données dans le tableau réactif est réorganisé. L'ordre dans lequel les éléments sont affichés est également mis à jour.

INFO

👉 On retrouve ici l’attribut key, important pour permettre un rendu correct des éléments lors de la réorganisation des données.

🐶 Dog Center

Nous souhaitons désormais ajouter une section « Actualités du chenil » en en-tête de notre galerie pour présenter les événements récents du centre. Les actualités doivent prendre la forme d’un carrousel, avec deux boutons permettant de passer à l’actualité suivante ou précédente. Chaque actualité doit présenter un titre, un contenu et une image de fond.

Commençons par définir les actualités dans un fichier src/newsData.js, similaire à src/dogsData.js:

js
export default [
  {
    id: 1,
    title: "Open House Day",
    content: "Join us for an open house day this weekend.",
    pictureUrl: "https://dog-center.com/api/news/1.png",
  },
  {
    id: 2,
    title: "Recent Adoptions",
    content: "Discover the stories of the dogs recently adopted.",
    pictureUrl: "https://dog-center.com/api/news/2.png",
  },
  ... 
];

Définissons maintenant un nouveau composant (src/components/NewsSlider) qui importera les données d'actualités et affichera les affichera sous la forme d'un carrousel :

jsx
import { useState } from "react";
import newsData from "../newsData.js";

export default function NewsSlider() {
  const [currentIndex, setCurrentIndex] = useState(0);

  const selectPrevious = () => {
    if (currentIndex > 0) {
      setCurrentIndex((prevIndex) => prevIndex - 1);
    }
  };

  const selectNext = () => {
    if (currentIndex < newsData.length - 1) {
      setCurrentIndex((prevIndex) => prevIndex + 1);
    }
  };

  return (
    <div>
      <h2>Dog Center News</h2>
      <div
        style={{
          backgroundImage: `url(${newsData[currentIndex].pictureUrl})`,
          backgroundSize: "cover",
          backgroundPosition: "center",
          padding: "20px",
          color: "grey",
        }}
      >
        <h3>{newsData[currentIndex].title}</h3>
        <p>{newsData[currentIndex].content}</p>
      </div>

      <div>
        <button onClick={selectPrevious} disabled={currentIndex === 0}>
          Previous
        </button>
        <button
          onClick={selectNext}
          disabled={currentIndex === newsData.length - 1}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Le composant définit une donnée réactive currentIndex contenant l'index de l'actualité actuellement sélectionnée. Le contenu HTML s'appuie sur cet index pour déterminer le titre, le contenu et l'arrière-plan à afficher.

Les boutons Previous et Next permettent de modifier l'état de la donnée currentIndex. Si l'on clique sur l'un des boutons pour modifier l'index, les données affichées sont automatiquement actualisées dans le composant.

TIP

💡 Des conditions sont déclarées au niveau de l'attribut disabled des deux boutons. Cela permet de désactiver l'un des boutons si l'index correspond à une actualité située à une extrémité du tableau de données.

Le composant peut finalement être importé dans le composant App, pour être inclus dans le reste de l'application.

Conclusion

Les données réactives sont un élément clé dans la gestion de la réactivité des applications frontend modernes. Elles permettent de facilement contrôler l'état d'une application et de réagir aux changements provoqués par les interactions des utilisateurs. Dans les prochains chapitres, nous verrons comment le framework s’appuie sur ce mécanisme pour faciliter les interactions avec un formulaire ou le filtrage de données.