Appearance
Données calculés
Dans une application frontend, il arrive que l’on souhaite déterminer une valeur en fonction d’autres données présentes dans l’application : il peut s’agir d’une somme de prix dans une liste de courses, ou les éléments d’une liste correspondant à une recherche.
À nouveau, la réactivité du framework permet de représenter simplement cette dépendance entre plusieurs valeurs, afin que les modifications des premières entraînent automatiquement le recalcul des secondes. Ces données, dont la valeur est calculée à partir d’autres, sont communément nommées données calculées (computed data).
Une variable exprimée par un calcul
Contrairement à des variables classiques auxquelles sont assignées des valeurs déterminées, les données calculées sont exprimées à l’aide d’une fonction. Cette fonction représente le calcul aboutissant à la valeur que doit prendre la donnée calculée :
jsx
import { useState, useMemo } from "react";
export default function App() {
const [number1, setNumber1] = useState(0);
const [number2, setNumber2] = useState(0);
const sum = useMemo(() => {
return number1 + number2;
}, [number1, number2]);
return (
<div>
<div>
<input
type="number"
value={number1}
onChange={(e) => setNumber1(Number(e.target.value))}
/>
</div>
<div>
<input
type="number"
value={number2}
onChange={(e) => setNumber2(Number(e.target.value))}
/>
</div>
<p>
{number1} + {number2} = {sum}
</p>
</div>
);
}Dans cet exemple, l’application propose l’édition de deux nombres via des champs de formulaire et affiche ensuite la somme des deux nombres.
La somme dépend des deux nombres choisis. Il est donc nécessaire de la recalculer chaque fois que les nombres changent. Pour exprimer cette dépendance, on peut utiliser la fonction useMemo de React.js. Cette fonction prend en premier argument une fonction qui effectue le calcul aboutissant à la valeur de sum. En second argument, elle prend la liste de toutes les données réactives dont dépend le calcul.
La fonction useMemo fait ce qu’on appelle de la mémoïsation (différente de la mémorisation). C’est-à-dire qu’elle garde en mémoire le résultat d’un calcul tant que les données d’entrée restent inchangées. Ici, le calcul de sum ne sera relancé que si l’une de ses dépendances, listées en second argument, est modifiée.
WARNING
⚠️ Attention, useMemo mémorise le résultat des calculs précédents. Si les valeurs de number1 et number2 reviennent dans un état précédent, le calcul ne sera pas relancé, ce qui optimise les performances de l’application. Il est donc important d’indiquer en second argument toutes les données pouvant influer sur le calcul, au risque d’obtenir un résultat incorrect si elles sont omises. La fonction doit également être pure, c'est-à-dire que son résultat ne doit dépendre que de ses arguments, ou plutôt ici des valeurs listées en tant que second argument.
Même si la variable sum est exprimée à l’aide d’une fonction, elle s’utilise comme n’importe quelle donnée réactive et non comme une fonction. Lorsque la valeur est affichée dans le code HTML, par exemple, la variable est simplement citée, et non appelée avec des parenthèses. L’exécution du calcul est pilotée par la modification des valeurs dont le calcul dépend, et non par un appel explicite dans le code.
INFO
👉 Une conséquence de cette dépendance est que la valeur des données calculées ne peut pas être modifiée directement. Il n’est pas possible d’assigner arbitrairement une nouvelle valeur à la variable sum, car elle doit toujours être le résultat de la fonction qui la définit.
Tableau filtré
Les données calculées permettent en particulier d’effectuer des opérations sur des jeux de données complexes, comme des tableaux que l’on souhaite filtrer ou réordonner :
jsx
import { useState, useMemo } from "react";
export default function App() {
const [search, setSearch] = useState("");
const [fruits, setFruits] = useState(
["Banana", "Orange", "Kiwi", "Apple"]
);
const filteredItems = useMemo(() => {
return fruits.filter((fruit) =>
fruit.toLowerCase().includes(search.toLowerCase())
);
}, [fruits, search]);
return (
<div>
<input
type="text"
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}La donnée calculée filteredItems permet ici de créer un tableau de fruits qui dépend à la fois d'un champ de recherche search et d'une liste de fruits fruits. Un changement de la donnée search entraîne automatiquement un recalcul de filteredItems, qui correspond aux éléments de fruits filtrés en fonction de la recherche.
À aucun moment, la valeur du tableau initial fruits n'est altérée, ce qui permet de toujours conserver la liste complète des fruits. Côté HTML, les fruits listés s'adaptent dynamiquement en fonction de la recherche effectuée par l'utlisateur.
🐶 Dog Center
Les données calculées nous permettront de finaliser la recherche et le tri de notre galerie de chiens.
Dans le chapitre précédent, nous avons défini deux données réactives, search et dogsSortBy, représentant la façon dont les chiens doivent être affichés dans la galerie.
Ajoutons une donnée calculée qui sera une version filtrée et réordonnée du tableau dogsData en fonction de ces critères :
jsx
import "./App.css";
import { useState, useMemo } from "react";
import dogsData from "./dogsData";
import NewsSlider from "./components/NewsSlider";
import DogCard from "./components/DogCard";
export default function App() {
const [search, setSearch] = useState("");
const [dogsSortBy, setDogsSortBy] = useState("age");
const filteredDogsData = useMemo(() => {
let result = dogsData.filter((dog) =>
dog.name.toLowerCase().includes(search.toLowerCase())
);
result = result.toSorted((a, b) => {
if (dogsSortBy === "age") {
// age can be null
return (a.age || 0) - (b.age || 0);
} else {
// sort in alphabetical order
return a.name.localeCompare(b.name);
}
});
return result;
}, [dogsData, search, dogsSortBy]);
return (
<div>
<h1>Dog Center</h1>
<NewsSlider />
<div id="gallery-options">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search dog"
/>
<label htmlFor="dog-sort">Sort by : </label>
<select
id="dog-sort"
value={dogsSortBy}
onChange={(e) => setDogsSortBy(e.target.value)}
>
<option value="age">Age</option>
<option value="name">Name</option>
</select>
</div>
<div id="dog-gallery">
{filteredDogsData.map((dog) => (
<DogCard
key={dog.id}
name={dog.name}
age={dog.age}
breed={dog.breed}
pictureUrl={dog.pictureUrl}
soundUrl={dog.soundUrl}
/>
))}
</div>
</div>
);
}La donnée calculée filteredDogsData filtre d'abord les chiens dont le nom correspond à la recherche. Ensuite, elle trie les chiens en fonction de leur âge ou de leur nom. Le tableau résultant de ces transformations est retourné pour déterminer la valeur de filteredDogsData.
Cette donnée est ensuite utilisée à la place de dogsData dans l'itération HTML, afin que la galerie affiche uniquement les chiens correspondant à la recherche, dans l'ordre défini par le tri.
Conclusion
Les données calculées permettent d'exploiter pleinement les possibilités des données réactives. Elles offrent un moyen déclaratif de déterminer la valeur de certaines données en fonction des autres.
Cela simplifie le calcul de valeurs complexes, notamment lorsque celles-ci dépendent de multiples critères, qui peuvent entraîner un recalcul de la valeur en cas de changement.
Dans le chapitre suivant, nous verrons une autre façon de réagir à un changement d'état, grâce à la mécanique des observateurs.

