• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 09/09/2020

Découvrez Redux

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous allons à présent aborder Redux. Il s'agit certainement de la partie la plus complexe de ce cours, alors surtout, ne vous découragez pas. Je vais tout faire pour simplifier au maximum son fonctionnement. On va y aller en douceur et si jamais tout n'est pas clair dans vos têtes, pas d'inquiétudes, on va pratiquer, pratiquer, pratiquer, jusqu'à ce que ce soit clair pour vous.

J'ai découpé l'apprentissage de Redux en trois chapitres. Un premier, où vous allez vous la couler douce, :lol: on ne va parler que de théorie. Puis un deuxième et un troisième, où l'on mettra tout ce que l'on a vu en pratique.

Avant de vous présenter Redux, vous devez vous demander pourquoi on doit aborder ce sujet ? Surtout si c'est complexe ?

Je suis pas venu ici pour souffrir, OK !

Pourquoi utiliser Redux ?

Pour comprendre le besoin d'utiliser Redux, il faut que je vous parle de la prochaine fonctionnalité que l'on va développer. Nous allons gérer les films favoris de l'utilisateur. La fonctionnalité est plutôt complète, mais logique et on peut la décomposer en 8 étapes :  

  1. L'utilisateur fait une recherche de film dans votre application et clique sur un film.

  2. L'utilisateur accède à la vue détail du film et clique sur le bouton ♡ pour ajouter le film à ses favoris.

  3. Le film est ajouté aux films favoris de l'utilisateur, le détail du film est re-rendu avec le bouton ♡ qui devient 🖤pour indiquer que le film est dans nos favoris.

  4. L'utilisateur revient sur la vue de recherche, le film ajouté aux favoris est marqué d'un 🖤à côté du titre du film pour indiquer, ici aussi, que le film est dans les favoris.

  5. L'utilisateur clique sur le film ajouté aux favoris.

  6. L'utilisateur accède à la vue détail du film, le bouton 🖤indique que le film fait partie des films favoris. L'utilisateur clique sur le bouton 🖤pour retirer le film des favoris.

  7. Le film est retiré des films favoris de l'utilisateur, le détail du film est re-rendu avec le bouton 🖤 qui devient ♡ pour indiquer que le film ne fait plus partie de nos favoris.

  8. L'utilisateur revient sur la vue de recherche, le film retiré des favoris n'est plus marqué d'un 🖤à côté du titre du film.

Ce n'est pas clair ? Je vous ai préparé un petit schéma pour décrire la fonctionnalité visuellement. ;) Ce schéma nous sera très utile quand on réalisera la fonctionnalité dans le prochain chapitre. J'ai ajouté des cercles rouges à chaque endroit où un clic a été réalisé. Les numéros au-dessus des écrans correspondent aux numéros des étapes ci-dessus. 

Fonctionnalité complète Ajout/Suppression film favoris
Fonctionnalité complète Ajout/Suppression film favori

C'est plus clair, cette fois ? :p Vous voyez la fonctionnalité que je souhaite mettre en place dans notre application ?

La difficulté de cette fonctionnalité réside dans le fait que je veux ajouter ou supprimer des films des favoris et que cet ajout ou cette suppression doit affecter plusieurs endroits de l'application, donc plusieurs components. Si j'ajoute un film aux favoris, je veux que le component FilmDetail soit re-rendu pour afficher le 🖤, mais je veux également que le component FilmItem, qui contient le film ajouté, de ma FlatList soit lui aussi re-rendu pour afficher le 🖤.

Je vais maintenant vous poser la question :

Avec tout ce que l'on a vu sur React Native jusque là, et éventuellement vos propres expériences en développement, comment envisageriez-vous de mettre en place cette fonctionnalité ? Et attention, je veux une solution propre ! 

Les solutions

La base de données

Utiliser une base de données pour stocker les films favoris. Pourquoi pas ? Lorsque l'utilisateur ajoute un film aux favoris, on l'ajoute dans une base de données et on change le rendu du bouton favoris en 🖤pour indiquer qu'il a bien été ajouté.

L'inconvénient est que, lorsque vous revenez sur la vue de recherche (étape 4 du schéma ci-dessus), il faut forcer le rechargement de la vue et aller chercher dans votre base de données les nouveaux films en favoris.

Le state

Utiliser le state pour stocker les films favoris. C'est une bonne idée. En manipulant le state et  setState, on n'a pas à forcer le rechargement de nos components. L'ajout aux favoris du film re-rend automatiquement le détail du film et on affiche cette fois le bouton favoris en 🖤pour indiquer qu'il a bien été ajouté.

Encore une fois, il y a un inconvénient. Vous allez devoir gérer deux states avec les films favoris, un dans le détail du film et un dans la vue de recherche.

Le state global

Utiliser un state global pour stocker les films favoris. Très bonne idée. Imaginez que nous puissions gérer un state global à toute notre application. Lorsque vous ajoutez un film aux favoris, celui-ci est ajouté dans votre state global. Puis, tous les components qui utilisent les films favoris de votre state global sont re-rendus automatiquement.

C'est exactement ce qu'il nous faut et c'est LA bonne solution !

En gérant un state de manière globale pour notre application, nous n'aurons pas à gérer la mise à jour des components qui utilisent les données du state. Bien sûr, dit comme cela, c'est facile ; en pratique, cela l'est un peu moins. J'ai été dans la même situation que vous en ce moment et, comme tout le monde, j'ai ouvert Google et j'ai tapé "react how to manage global state". En cherchant un peu, je suis tombé de plus en plus souvent sur un mot inconnu, mais qui avait l'air de répondre à mes attentes, il s'agit de Redux.

C'est quoi, Redux ?

Redux est une librairie Javascript créée par deux développeurs qui, en utilisant React, ont été confrontés au même problème que nous en ce moment. Ils souhaitaient pouvoir gérer un state global dans leur application et proprement. N'ayant pas trouvé chaussure à leur pied, ils ont carrément créé leur propre librairie en se basant sur Flux. Cela en fait, des noms à retenir:lol:

Flux

Flux
Flux

Créé par facebook, Flux est une architecture permettant de gérer les données des applications. L'architecture est basée sur un principe simple, le flux de données est unidirectionnel. C'est ce que l'on appelle en architecture fonctionnelle le one-way data flow, le flux de données va toujours dans le même sens : 

Architecture de Flux
Architecture de Flux

À première vue, on ne nous a pas menti, c'est bien unidirectionnel. Toutes les flèches ne vont que dans un sens. Ensuite, il faut bien comprendre chaque élément :

  • Action : Une action est un flux d'information que l'on souhaite envoyer à notre state global. 

  • Dispatcher : Le dispatcher a pour fonction de transmettre l'action au store. Avec l'architecture Flux, il peut y avoir un ou plusieurs stores et c'est au dispatcher de transmettre l'action au bon store.

  • Store : Un store est un objet qui possède et gère un state de votre application. Avec Flux, il peut y avoir plusieurs stores, gérant chacun un type de donnée. Ici, il reçoit une action du dispatcher et effectue le traitement nécessaire sur son state en fonction de l'action reçue. On pourrait modifier la modélisation du store ci-dessus par ceci :

    Store Flux
    Store Flux avec le state de votre application (state global)
  • View : Une vue est ce que l'utilisateur voit, ce sur quoi il interagit. En React, une View correspond à un component. Ce sont donc nos components qui vont émettre des actions. 

Vous comprenez le fonctionnement de l'architecture Flux ? Vous voyez que tout tourne dans un sens, toujours. Avec cette architecture, vos components ne modifient jamais directement le state de votre application. Ils émettent une action et laisse le dispatcher et le store choisi s'occuper du reste. Ensuite, une fois le state de votre application mis à jour par le store, vos components se re-rendent pour prendre en compte les modifications du state. Grâce à ce fonctionnement, votre application possède à présent un state global et est capable de mettre à jour plusieurs components qui partagent les mêmes données, en même temps.

Cela devrait nous permettre de réaliser notre fonctionnalité d'ajout de film aux favoris. :)

Redux

Bon, c'est bien gentil, Flux, on sait comment ça marche, mais Redux, dans tout ça ? On est venu pour cela, à la base.

Redux s'appuie sur l'architecture Flux, tout en simplifiant certains concepts : 

  • Redux ne permet pas de gérer plusieurs stores. Vous n'aurez qu'un seul store avec toutes les données de votre application, c'est-à-dire tout votre state global.

  • Étant donné qu'il n'y a plus qu'un store, le dispatcher est inutile et n'existe pas sur Redux. C'est le store lui-même qui récupère l'action envoyée.

Voici donc, schématiquement, le fonctionnement de la librairie Redux :

Architecture utilisée par la librairie Redux
Architecture utilisée par la librairie Redux

Votre View crée et envoie une action. Cette action est récupérée par le store. Le store modifie le state de votre application en fonction de l'action reçue. Votre View détecte les changements du state global et se re-rend. Beaucoup plus simple que Flux, n'est-ce pas ? :D En plus, on garde toujours ce fonctionnement avec un flux de données à sens unique, propre au one-way data flow et à Flux.

Bon, je vous avoue que j'ai un peu simplifié le fonctionnement de Redux ici. Je vous ai dit que le store modifiait le state de votre application. Et bien... ce n'est pas tout à fait vrai. Les modifications du state passent par des reducers

Reducer

Un reducer est une fonction créée dans le store. Cette fonction a pour rôle de modifier le state de votre application en fonction d'une action reçue. Ce n'est donc pas le store qui modifie directement le state de votre application, mais un reducer contenu dans le store. 

Ce n'est pas hyper clair ? :euh: Pas de soucis, je vous donne un exemple où l'on applique à la lettre le fonctionnement de Redux et de ses reducers.

Dans mon application, l'utilisateur souhaite ajouter un film à ses favoris. Voici ce qu'il se passe :

  1. L'utilisateur clique sur le bouton pour ajouter le film aux favoris, une action est créée.

  2. L'action est envoyée au store.

  3. Le store possède un ou plusieurs reducers. Un des reducers sait comment gérer cette action, il fait signe au store de lui donner l'action et le state actuel de l'application.

  4. Le reducer fait ce qu'il a à faire, il modifie le state de l'application en ajoutant le film aux favoris.

  5. Le reducer redonne au store le state mis à jour avec le nouveau film favori.

  6. Les components qui utilisent le state global de l'application sont informés qu'un changement a eu lieu et se re-rendent.

Allez, vous avez été sympa jusque là, voici un schéma certainement plus parlant : :lol: (Comme à mon habitude, j'ai fait correspondre les numéros ci-dessus avec des actions du schéma.)

Fonctionnement de Redux lors de l'ajout d'un film aux favoris
Fonctionnement de Redux lors de l'ajout d'un film aux favoris

Un reducer est donc une fonction qui modifie le state de votre application en fonction d'une action.

Voyons maintenant à quoi ressemble un reducer.

La syntaxe d'un reducer est plutôt simple. Je vous en ai parlé juste au-dessus, le reducer demande au store de lui fournir le state actuel et l'action à gérer. Logiquement, sa déclaration ressemble à ceci :

function monReducer (state, action) {
// Modification du state en fonction de l'action ici
return nextState // Renvoie le state mis à jour
}

Pas de surprises ici. :) Un reducer est une fonction qui prend en paramètre le state de l'application et l'action à gérer.

Plusieurs reducers

Un store peut avoir un ou plusieurs reducers. Généralement, chaque reducer gère une fonctionnalité de votre state global.

Imaginez le cas où vous souhaitez gérer les films favoris et le profil de l'utilisateur dans votre state global. Ce sont, en soi, deux fonctionnalités différentes. Dans ce cas, vous allez créer deux reducers, un pour gérer les films favoris et un pour gérer le profil de l'utilisateur.

Le store de votre application ressemblera à cela :

Exemple d'un store avec plusieurs reducers
Exemple d'un store avec plusieurs reducers

À présent, une action est créée dans votre application. Cette action concerne l'ajout d'un film aux favoris de votre state global. Logiquement, le reducer en charge des films favoris va récupérer l'action et ajouter le film à votre state global.

Cas où une action d'ajout de film favoris au state global arrive au niveau du store
Cas où une action d'ajout de film favori au state global arrive au niveau du store

Si, à l'inverse, l'action concerne une modification du profil utilisateur, c'est le reducer en charge du profil utilisateur qui va prendre en charge l'action et mettre à jour le state global avec le nouveau profil utilisateur.

Cas où une action de modifier du profil utilisateur dans le state global arrive au niveau du store
Cas où une action de modifier le profil utilisateur dans le state global arrive au niveau du store

Voilà, vous savez à peu près tout sur les reducers. :soleil: Cela fait beaucoup d'informations à retenir, j'en suis conscient. Encore une fois, j'insiste. Si ce n'est pas parfaitement clair dans vos têtes, ne vous inquiétez pas, cela le sera lorsque l'on mettra en pratique Redux dans notre application.

Pour l'instant, retenez qu'avec Redux :

  • Le store ne modifie pas directement le state de votre application, ce sont les reducers qui s'en occupent.

  • Un reducer est une fonction qui modifie le state de l'application en fonction d'une action.

  • Un store peut avoir un ou plusieurs reducers.

  • Chaque reducer gère une fonctionnalité de votre state global.

Bien, mine de rien, ici, on vient de voir ensemble tout le fonctionnement de Redux. Bravo, vous avez tenu le coup. :pirate: Mais ne partez pas si vite, il nous reste une dernière chose à voir.

Je vous ai dit que le reducer modifiait le state de votre application en fonction d'une action. C'est bien joli tout ça, mais, cela ressemble à quoi, concrètement, une action ?

Construction d'une action

Une action est, tout simplement, un objet avec deux paramètres :

  • un paramètre type qui correspond au type d'action : ajout aux favoris, suppression des favoris, modification du profil, etc.

  • un paramètre value qui correspond à l'objet que l'on souhaite modifier dans le state : un film, le profil utilisateur, etc.

Une fois l'action construite, vous connaissez la suite. ;) Elle est transmise au store, qui la donne au reducer apte à la gérer. Mais là encore, une interrogation se pose.

Comment le reducer gère-t-il les actions et comment sait-il que cette action est pour lui ?

Pour gérer les actions, dans un reducer, on utilise un switch case , comme ceci :  

function monReducer (state, action) {
let nextState
switch(action.type) {
case 'ACTION_1':
// Modification du state de l'application
return nextState
case 'ACTION_2':
// Modification du state de l'application
return nextState
//...
default:
return state
}
}

Bon, c'est vrai que, vu comme cela, ce n'est pas très parlant. Prenons l'exemple de mon reducer pour gérer le profil utilisateur. Voici à quoi mon reducer va ressembler :

function reducerProfil (state, action) {
let nextState
switch(action.type) {
case 'ADD_PROFIL':
// Ajout du profil contenu dans action.value au state de mon application
return nextState
case 'UPDATE_PROFIL':
// Modification du profil contenu dans action.value dans le state de mon application
return nextState
case 'DELETE_PROFIL':
// Suppression du profil contenu dans action.value du state de l'application
return nextState
default:
return state
}
}

C'est plus clair pour vous. :) Cet exemple est parfait, car il vous montre qu'un reducer peut gérer plusieurs actions différentes

Grâce à ce  switch case  , votre reducer sait quels types d'actions il est apte à gérer :

  • Si une action avec un type "ADD_PROFIL" arrive dans votre store, le reducer ici présent saura qu'il est capable de la gérer et va prendre en charge son traitement.

  • Si une action avec un type "ADD_FAVORITE_FILM" arrive dans votre store, le reducer ici présent saura que cette action n'est pas pour lui et ne la traitera pas.

C'est tout ce qu'il y a à savoir sur les reducers et les actions.

Attends, attends, pourquoi, dans le reducer, des fois, tu renvoies  nextState  et des fois  state  ? C'est fait exprès ?

Bien vu. ;) Vous vous doutez bien que je n'ai pas fait cela par hasard. En fait, les reducers doivent répondre à une règle de codage : le state doit toujours rester immuable

Immuable

Un objet immuable, immutable en anglais, est un objet que l'on ne peut pas modifier après sa création. Si vous souhaitez modifier un objet immuable, il faut créer une copie de cet objet (donc créer un nouvel objet) et y appliquer vos modifications.

D'accord, mais cela change quoi pour notre state qu'il soit immuable ? 

Cela signifie que, dans vos reducers, vous ne devez jamais modifier directement le state de votre application. Autrement dit, ne jamais faire : :colere:

state.profil = action.value

ou encore

state = { profil: action.value }

Si vous faites cela, vous modifiez directement le state de votre application.

Pour respecter le principe d'immuable, vous devez créer un nouvel objet dans lequel vous allez copier votre state et appliquer vos modifications, comme ceci :

let nextState
nextState = {
...state,
profil: action.value
}

 Ici, on a créé un nouvel objet  nextState   dans lequel on a copié le state de l'application   ...state . Rappelez-vous, la syntaxe  ...state  avec ES6 permet de créer une copie d'un objet. Puis, on a appliqué notre modification, ici une modification de profil  profil: action.value .

On obtient donc un nouvel objet  nextState  qui correspond à notre nouveau state. Notre state de base (  state  ) est resté inchangé. On respecte bien le principe d'immuable. 

Si, maintenant, j'applique ce principe dans mon application, voici ce à quoi nos reducers vont ressembler :

function monReducer (state, action) {
let nextState
switch(action.type) {
case 'ACTION_1':
nextState = {
...state,
value: action.value
}
return nextState
case 'ACTION_2':
nextState = {
...state,
value: action.value
}
return nextState
//...
default:
return state
}
}
  • Dans le cas où mon reducer gère le type d'action, je crée un nouveau state  nextState  avec le state mise à jour et je le renvoie  return nextState.

  • Dans le cas où mon reducer ne gère pas le type d'action, cas  default  , je n'ai pas modifié mon state, je peux le renvoyer tel quel  return state.

setState

OK, pourquoi crées-tu une catégorie  setState  ici ? C'est quoi le rapport ?

Je fais une petite parenthèse, mais cela me semble être une information intéressante.setState  applique à la lettre le principe d'immuable. On ne s'en rend pas compte lorsqu'on l'utilise, car c'est React qui gère tout ça.

Si vous jetez un œil à la documentation de setState, vous pouvez y lire : 

setState(stateChange[, callback])

This performs a shallow merge of  stateChange  into the new state

Cela signifie que, lorsque vous utilisez  setState(value: value)  , React va créer une copie de votre state, y appliquer vos changements ( value ) et retourner un nouvel objet state.

Cela ne va pas changer notre façon d'utiliser  setState, mais c'est toujours bon à savoir. ^^

Nous arrivons à la fin de cette présentation de Redux. J'espère que je ne vous ai pas perdu le long de ce chapitre. :euh: C'était long et compliqué, mais il fallait passer par là pour continuer notre apprentissage. Vous verrez que le plus dur est derrière nous, à présent.

Redux va donc nous permettre de gérer un state global avec, pour l'instant, les films favoris de notre application. Ces films seront gérés par un reducer que l'on va créer dans le prochain chapitre. Lorsque l'on modifiera les films favoris via le reducer, le state de notre application sera mis à jour et tous les éléments graphiques qui en dépendent seront automatiquement mis à jour. C'est le principe du state en React, après tout. :)

Pour faire fonctionner Redux, nous allons devoir mettre en place toute son architecture dans notre application. La mise en place est un peu longue, il y a pas mal de fichiers à créer et modifier, mais une fois l'architecture construite, on développe facilement et rapidement de nouvelles fonctionnalités.

Exemple de certificat de réussite
Exemple de certificat de réussite