Comprenez ce qu’est un EntityAdapter
Nous faisons bien souvent des applications avec la même approche, et des fonctionnalités qui se rapprochent bien plus qu’il ne nous paraît.
Si nous stockons une liste de “choses”, il est évident qu’une des fonctionnalités sera d’ajouter une “chose” à cette liste. De plus, si nous stockons une liste, nous souhaitons généralement l’afficher.
Dans notre cas, imaginons qu’un client ayant des allergies alimentaires veuille pouvoir indiquer les aliments à retirer de son burger. Pour ce faire, nous pouvons ajouter la fonctionnalité d’ajout de notes qui seront affichées aux cuisiniers de notre restaurant.
Nous pourrions ajouter une slice, des sélecteurs… ou utiliser un outil qui nous permettrait de simplifier la configuration !
Vous voyez où je veux en venir ? Je parle bien du CRUD :
Create (créer),
Read (lire),
Update (mettre à jour),
Delete (supprimer).
Ce sont des opérations que nous pouvons qualifier de génériques. C’est-à-dire que ce sont des actions qui ont des comportements identiques mais qui sont appliquées à des objets différents.
Je vous mets un petit exemple dans le code suivant :
const houses = [
{
id: 1,
name: 'house1',
price: 100000
},
{
id: 2,
name: 'house2',
price: 200000
},
{
id: 3,
name: 'house3',
price: 300000
}
]
const students = [
{
id: 1,
name: 'student1',
average: 20
},
{
id: 2,
name: 'student2',
average: 18
},
{
id: 3,
name: 'student3',
average: 19
}
]
const getHouses = () => {
return houses
}
const getStudents = () => {
return students
}
const getHouseById = (id) => {
return houses.filter(house => house.id === id)
}
const getStudentById = (id) => {
return students.filter(student => student.id === id)
}
const addHouse = (house) => {
houses.push(house)
}
const addStudent = (student) => {
students.push(student)
}
Ci-dessus, students
et houses
sont des listes d’objets différents. À première vue, on ne peut pas avoir les mêmes actions. Mais en prenant un peu de hauteur, addStudent
et addHouse
font la même chose, ajouter un élément dans la liste : students
pour addStudent
et houses
pour addHouse
.
On peut à priori simplifier tout ça.
Je vous propose la petite implémentation ci-après :
const entityAdapter = () => {
const entities = [];
return {
addOne: (entity) => {
entities.push(entity)
return this
},
selectAll: () => entities,
selectById: (id) => entities.find(entity => entity.id === id),
}
}
const students = entityAdapter()
const houses = entityAdapter()
Nos deux constantes students
et houses
ont maintenant des actions associées ( addOne
, selectAll
etselectById
). Plus besoin de les créer une à une, grâce à un outil inclus dans Redux Toolkit : EntityAdapter
.
Cet outil permet de configurer notre store et de générer les outils qui vont nous permettre d’effectuer simplement ces opérations CRUD.
L’ EntityAdapter
est une fonction qui génère un ensemble de reducers et de sélecteurs destiné à effectuer des opérations CRUD. Avec cet outil, nous aurons accès à des méthodes comme addOne
pour ajouter une entrée, addMany
pour ajouter plusieurs entrées, setOne
pour modifier une entrée, etc.
Créez un EntityAdapter
Pour utiliser cette fonctionnalité, utilisons createEntityAdapter
de Redux Toolkit.
Cette fonction s’emploie un peu comme dans notre implémentation simplifiée vue dans le paragraphe juste au-dessus, avec la particularité de pouvoir surcharger chaque action du CRUD. Pour ce faire, on peut passer en paramètre un objet qui reprend chaque action devant être adaptée. Chaque action peut ainsi être adaptée en cas de besoin.
Par exemple, admettons que l’ id
de nos élèves ne soit pas stocké dans student.id
mais dans student.studentId
. Notre précédente implémentation ne fonctionnerait pas pour students
.
En effet, cette fonction ne retournera aucun résultat, car entity.id
sera toujours undefined
:
selectById: (id) => entities.find(entity => entity.id === id),
Il nous faut donc un moyen d’adapter notre selectById
selon l’entité, mais uniquement pour cette fonction.
Avec createEntityAdapter
, nous n’avons qu’à ajouter la fonction qui va être appliquée au moment de faire student.selectById
, en passant un objet comme ceci :
{
selectById: (students, id) => students.find(student => student.studentId === id),
}
Ce qui nous donne le code suivant :
import { createEntityAdapter } from '@reduxjs/toolkit'
const students = createEntityAdapter({
selectById: (students, id) => students.find(student => student.studentId === id),
})
createEntityAdapter
va donc générer un objet, students
, avec des actions prédéfinies.
Notre fonction selectById
sera exécutée lorsque nous ferons appel à students.selectById
et nous aurons bien un résultat pour un id
donné.
Pour ajouter notre entityAdaptor
à notre store, rien de plus simple. Comme je vous l’ai dit, il génère notre reducer et nos sélecteurs. Ci-dessous, je vous montre comment l’ajouter au store et comment extraire les sélecteurs :
const students = createEntityAdapter({
selectById: (students, id) => students.find(student => student.studentId === id),
})
const studentsSlice = createSlice({
...,
reducers: {
add: students.addOne,
}
})
const store = configureStore({
reducer: {
students: studentsSlice.reducer,
},
})
const studentsSelectors = students.getSelectors(state => state.students)
À vous de jouer
C’est maintenant l’occasion d’ajouter une fonctionnalité à notre store en employant EntityAdapter
.
Je vous propose de reprendre ma proposition qui est de permettre au client d’ajouter des notes pour qu’en cuisine, les préparateurs puissent suivre ces notes et faire des burgers sur mesure.
Qui ne connaît pas un proche ne mangeant pas d’oignons ?
Pour cela, je vous propose d’ajouter un dossier features/notes
, un composant features/notes/Notes.js
et une slice features/notesSlices.js
.
Ajoutez le composant features/notes/Notes.js
dans notre fichier app/App.js
.
Puis implémentez notre fonctionnalité avec les users stories suivantes :
En tant que client, je peux ajouter une note en remplissant un champ sur plusieurs lignes. En cliquant sur le bouton “Ajouter une note”, la note est sauvegardée.
En tant que client, je peux lister les notes que j’ai saisies. Un bouton “Supprimer” à droite de chaque note permet de retirer la note correspondante.
Une fois que vous avez implémenté tout cela, suivez-moi dans la solution :
En résumé
L'
EntityAdapter
est conçu pour gérer les opérations CRUD (Create, Read, Update, Delete) de manière générique.createEntityAdapter
de Redux Toolkit permet de configurer facilement un adaptateur pour n'importe quel type d'entité et offre la flexibilité de surcharger ou d'adapter les actions CRUD selon les besoins spécifiques de chaque entité.Utiliser les
EntityAdapter
permet d’implémenter avec peu d’efforts nos nouvelles fonctionnalités.
Nous voilà outillés pour créer de nouvelles fonctionnalités à la chaîne. Nous maîtrisons les EntityAdapter
, nos slices fonctionnent à merveille.
Mais une chose vous turlupine sans doute. JavaScript est un langage qui s’exécute de façon asynchrone dans la plupart des cas, les appels réseaux par exemple. Jusqu’ici, nous n’avons pas abordé cet angle. Eh bien dans la partie suivante, nous allons nous intéresser à l’asynchronisme dans notre store. Prêt pour voyager dans le temps ? 😂