• 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

Construisez une action sur Redux

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

Nous avons presque mis en place toute l'architecture de Redux. On touche au but, tenez bon. :lol:

Notre store Redux et son reducer sont prêts et ils sont capables de fournir le state global à nos components abonnés. Ils attendent à présent qu'on leur envoie une action pour ajouter ou supprimer un film des favoris et donc mettre à jour le state global. C'est ce que nous allons réaliser dans ce chapitre. 

Nous allons créer et lancer une action lorsque l'utilisateur clique sur le bouton favoris.

Pour l'instant, on va créer un bouton favoris tout simple juste au-dessous du titre du film dans le détail d'un film. On le customisera par la suite, quand on en aura fini avec Redux : :pirate:

// Components/FilmDetail.js
import { ..., Button } from 'react-native'
class FilmDetail extends React.Component {
// ...
_toggleFavorite() {
// Définition de notre action ici
}
_displayFilm() {
const { film } = this.state
if (film != undefined) {
return (
<ScrollView style={styles.scrollview_container}>
<Image
style={styles.image}
source={{uri: getImageFromApi(film.backdrop_path)}}
/>
<Text style={styles.title_text}>{film.title}</Text>
<Button title="Favoris" onPress={() => this._toggleFavorite()}/>
//...
</ScrollView>
)
}
}
//...
}

Créez une action

Passons à la création de notre action. Souvenez-vous de ce que l'on a dit sur le contenu d'une action et sur ce qu'attend votre reducer. Votre action doit posséder un type et une valeur. Dans notre fonctionnalité, le type de l'action est "TOGGLE_FAVORITE" et la valeur est le film affiché. On a donc :

// Components/FilmDetail.js
_toggleFavorite() {
const action = { type: "TOGGLE_FAVORITE", value: this.state.film }
}

Jusque là, vous me suivez. ^^ Ensuite, si on suit l'architecture de Redux, on doit envoyer notre action au store.  

Comment envoyer une action au store Redux ?

Rebelote et direction la documentation de react-redux. On y apprend que le store Redux dispose d'une fonction  dispatch  qui a la même fonction qu'un dispatcher, et qui permet de distribuer notre action au store et à ses reducers. Parfait, c'est ce qu'il nous faut. :)

Cette fois, il faut définir le deuxième paramètre de la fonction  connect  , à savoir la fonction  mapDispatchToProps .

Je vous mets le code, mais ne le faites pas, je vous explique pourquoi juste après : 

// Components/FilmDetail.js
const mapDispatchToProps = (dispatch) => {
return {
dispatch: (action) => { dispatch(action) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(FilmDetail)

Puis vous n'avez plus qu'à appeler  this.props.dispatch(action)  pour donner l'action au store et qu'il se charge de "dispatcher" l'action au bon reducer. 

Pourquoi je ne dois pas le faire ?

Tout simplement parce que Redux le fait déjà par défaut. Dès lors que vous utilisez la fonction  connect  sur un component, Redux va mapper la fonction  dispatch  à votre component. 

Vous l'avez peut-être aperçu tout à l'heure, quand on a affiché les props dans les logs, juste après avoir mappé le state de notre application aux props de notre component. À ce moment-là, on avait ceci :

09:36:58: Object {
09:36:58:   "favoritesFilm": Array [],
09:36:58:   "dispatch": [Function dispatch],
09:36:58:   ...

Elle est là, notre fonction  dispatch  ! :ninja:

On a donc accès à la fonction  dispatch  du store Redux depuis le début, en tout cas depuis que l'on a utilisé la fonction  connect  sur notre component FilmDetail. La suite est simple, on va envoyer (dispatcher en anglais) notre action au store de Redux : 

_toggleFavorite() {
const action = { type: "TOGGLE_FAVORITE", value: this.state.film }
this.props.dispatch(action)
}

Cette fois, c'est fini, votre architecture est en place et fonctionnelle. Étant donné qu'il ne faut jamais me croire sur parole, on va vérifier ensemble que cela fonctionne.

Redux à l’œuvre

Créez la méthode  componentDidUpdate  dans le component FilmDetail et ajoutez des logs pour afficher les films favoris. Comme ça, je vous prouve non seulement que la liste des films favoris est bien mise à jour, mais également que l'on passe bien dans le cycle de vie updating de notre component;) Je vous mets le code complet de la classe pour que l'on teste bien la même chose :

// Components/FilmDetail.js
import React from 'react'
import { StyleSheet, View, Text, ActivityIndicator, ScrollView, Image, Button } from 'react-native'
import { getFilmDetailFromApi, getImageFromApi } from '../API/TMDBApi'
import moment from 'moment'
import numeral from 'numeral'
import { connect } from 'react-redux'
class FilmDetail extends React.Component {
constructor(props) {
super(props)
this.state = {
film: undefined,
isLoading: true
}
}
componentDidMount() {
getFilmDetailFromApi(this.props.navigation.state.params.idFilm).then(data => {
this.setState({
film: data,
isLoading: false
})
})
}
componentDidUpdate() {
console.log("componentDidUpdate : ")
console.log(this.props.favoritesFilm)
}
_displayLoading() {
if (this.state.isLoading) {
return (
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
</View>
)
}
}
_toggleFavorite() {
const action = { type: "TOGGLE_FAVORITE", value: this.state.film }
this.props.dispatch(action)
}
_displayFilm() {
const { film } = this.state
if (film != undefined) {
return (
<ScrollView style={styles.scrollview_container}>
<Image
style={styles.image}
source={{uri: getImageFromApi(film.backdrop_path)}}
/>
<Text style={styles.title_text}>{film.title}</Text>
<Button title="Favoris" onPress={() => this._toggleFavorite()}/>
<Text style={styles.description_text}>{film.overview}</Text>
<Text style={styles.default_text}>Sorti le {moment(new Date(film.release_date)).format('DD/MM/YYYY')}</Text>
<Text style={styles.default_text}>Note : {film.vote_average} / 10</Text>
<Text style={styles.default_text}>Nombre de votes : {film.vote_count}</Text>
<Text style={styles.default_text}>Budget : {numeral(film.budget).format('0,0[.]00 $')}</Text>
<Text style={styles.default_text}>Genre(s) : {film.genres.map(function(genre){
return genre.name;
}).join(" / ")}
</Text>
<Text style={styles.default_text}>Companie(s) : {film.production_companies.map(function(company){
return company.name;
}).join(" / ")}
</Text>
</ScrollView>
)
}
}
render() {
return (
<View style={styles.main_container}>
{this._displayLoading()}
{this._displayFilm()}
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1
},
loading_container: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
},
scrollview_container: {
flex: 1
},
image: {
height: 169,
margin: 5
},
title_text: {
fontWeight: 'bold',
fontSize: 35,
flex: 1,
flexWrap: 'wrap',
marginLeft: 5,
marginRight: 5,
marginTop: 10,
marginBottom: 10,
color: '#000000',
textAlign: 'center'
},
description_text: {
fontStyle: 'italic',
color: '#666666',
margin: 5,
marginBottom: 15
},
default_text: {
marginLeft: 5,
marginRight: 5,
marginTop: 5,
}
})
const mapStateToProps = (state) => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmDetail)

Retournez sur votre application, accédez au détail d'un film et cliquez plusieurs fois sur le bouton "Favoris". Vous devriez voir une succession de :

16:07:29: componentDidUpdate
16:07:29: Array []
16:07:31: componentDidUpdate
16:07:31: Array [
16:07:31:   Object {
16:07:31:     "adult": false,
16:07:31:     "backdrop_path": "/c4zJK1mowcps3wvdrm31knxhur2.jpg",
16:07:31:     "belongs_to_collection": Object {
16:07:31:       "backdrop_path": "/d8duYyyC9J5T825Hg7grmaabfxQ.jpg",
16:07:31:       "id": 10,
16:07:31:       "name": "Star Wars - Saga",
16:07:31:       "poster_path": "/ijAC17I7X4xCrKxtp1JUc2YBGuM.jpg",
16:07:31:     },
16:07:31: ...
16:07:33: componentDidUpdate
16:07:33: Array []
16:07:33: ...

Cela fonctionne. Non seulement la liste des films en favoris est bien mise à jour, on passe d'un tableau vide à un tableau avec un film, mais en plus votre component FilmDetail passe bien dans le cycle updating à chaque changement. Ce n'était pas facile, mais on a réussi. :soleil:

Récapitulatif

Même si c'est peut-être déjà clair dans vos têtes, je pense que cela ne fait pas de mal de faire un petit récapitulatif de tout ce qu'il se passe ici en termes de communication et d'échanges de données :

  1. L'utilisateur clique sur le bouton "Favoris" dans le component FilmDetail.

  2. On crée une action avec le type "TOGGLE_FAVORITE"  et en valeur, le film affiché.

  3. On fait passer l'action au store Redux.

  4. Le store Redux donne l'action (dispatch) à un reducer capable de gérer l'action de type "TOGGLE_FAVORITE".

  5. Le reducer  toggleFavorite  va recevoir l'action et va modifier le state de votre application.

  6. Redux va détecter un changement dans son store, sur la liste des films favoris, et va envoyer la nouvelle liste de films favoris aux components abonnés à ses changements.

  7. Le component FilmDetail reçoit la liste des nouveaux films, la mappe à ses props et lance le cycle de vie updating pour se re-rendre.

Je vous remets également le schéma de l'architecture Redux, vu dans le précédent chapitre, pour comparer et constater que notre architecture mise en place est bonne :

Architecture de Redux
Architecture de Redux

Au fait, j'avais presque oublié. Félicitations ! :ange: Vous avez créé une architecture complexe avec Redux pour gérer un state global. À présent, un large champ de possibilités s'offre à vous pour vos projets.

Il nous reste quelques ajustements à faire pour que notre gestion des films favoris soit terminée :

  • Modifier le bouton d'ajout / suppression d'un film des favoris par une image 🖤

  • Utiliser ce nouveau bouton pour gérer la présence ou non du film dans les favoris : 🖤/ ♡

  • Affichage de l'image 🖤 à côté des films favoris dans l'écran de recherche

Mettons du 🖤 dans notre application

Pour cet exercice, je vous ai préparé les deux images que l'on va utiliser pour ajouter / supprimer un film des favoris. Vous pouvez les récupérer et les placer dans un dossier /Images à la racine de votre application :

Image film favoris
Image film favori
Image film non favori
Image film non favori

Appelez respectivement les images : ic_favorite.png et ic_favorite_border.png.

Vue détail d'un film

À présent, on va modifier le bouton d'ajout / suppression des favoris, de la vue détail du film (FilmDetail), par cette image.

Les boutons sur React Native, Button, sont très peu customisables, voire pas du tout. On ne peut pas créer un Button avec une image, par exemple. De ce fait, on utilise souvent les Touchable pour créer nos boutons custom. 

Ici, on va à nouveau utiliser uneTouchableOpacity. Dans votre component FilmDetail, remplacez :

// Components/FilmDetail.js
<Button title="Favoris" onPress={() => this._toggleFavorite()}/>

par :

// Components/FilmDetail.js
<TouchableOpacity
style={styles.favorite_container}
onPress={() => this._toggleFavorite()}>
{this._displayFavoriteImage()}
</TouchableOpacity>

N'oubliez pas l'import de TouchableOpacity.

Pour la définition du style :

// Components/FilmDetail.js
favorite_container: {
alignItems: 'center', // Alignement des components enfants sur l'axe secondaire, X ici
}

Enfin, nous allons écrire la fonction  _displayFavoriteImage  qui va afficher :

  • un ♡ si le film n'est pas dans nos favoris ;

  • un 🖤si le film est dans nos favoris.

Là, c'est purement du Javascript avec un peu de JSX pour rendre l'image :

// Components/FilmDetail.js
_displayFavoriteImage() {
var sourceImage = require('../Images/ic_favorite_border.png')
if (this.props.favoritesFilm.findIndex(item => item.id === this.state.film.id) !== -1) {
// Film dans nos favoris
sourceImage = require('../Images/ic_favorite.png')
}
return (
<Image
style={styles.favorite_image}
source={sourceImage}
/>
)
}
// ...
// Dans les styles
favorite_image: {
width: 40,
height: 40
}

Vous pouvez tester sur votre application, allez-y ! Essayez d'ajouter un film à vos favoris, revenez à l'écran de recherche, retournez sur le film, etc. Bref, jouez avec l'application pour tester le maximum de cas. Vous verrez qu'à chaque fois, si le film est dans nos favoris, le bouton affiche 🖤, sinon il affiche ♡. On s'en est super bien sorti ici, cela fonctionne parfaitement ! :D

Ajout d'un film aux favoris avec Redux
Ajout d'un film aux favoris avec Redux

Vue recherche

Cette fois, c'est à vous. :-° Je vous demande ici de gérer les films favoris dans la vue recherche Search.js. La fonctionnalité est plutôt simple :

  • Si le film est dans les favoris, il faut afficher un 🖤à gauche du titre du film.

  • Si le film n'est pas dans les favoris, il ne faut rien afficher à gauche du titre du film.

Graphiquement, vous devez obtenir ceci  :

Vue recherche film en favoris (à gauche), non favoris (à droite)
Vue recherche film en favoris (à gauche), non favoris (à droite)

Avant de vous lancer, voici quelques recommandations et astuces. Vous avez ici deux solutions pour réaliser cette fonctionnalité.

La première consiste à connecter le store Redux au component FilmItem ; c’est la solution la plus simple, mais pas la plus optimisée. Avec cette solution, vous allez connecter le store Redux autant de fois qu’il y a de components FilmItem. Autrement dit, si vous affichez 100 films, vous allez connecter 100 fois le store Redux, ce n’est pas terrible.

La deuxième solution, plus propre, mais plus complexe, consiste à connecter le store Redux une seule fois dans le component Search, puis de faire passer à nos components FilmItem une prop  isFilmFavorite  (qui vaut  true  ou  false  ) qui affichera, ou non, le 🖤dans nos items.

Il n’y a rien de si compliqué là-dedans ? Il est où, le piège ? 🧐

Le piège, ce sont les listes de données sur React Native, les FlatList. Les listes de données sont pensées pour afficher énormément de données, des centaines d’items, voire des milliers. De ce fait, elles se doivent d’être optimisées au maximum, sans quoi notre application en serait ralentie.

Parmi les optimisations qu’elles possèdent, il y en a une qui peut se traduire par :

Moi, FlatList, je ne me re-rend que si l’on me le demande ET si mes données ont changé.

Vous voyez où je veux en venir ? Revenons à notre application et à la fonctionnalité que l’on souhaite mettre en place ici, ce sera plus simple. ;)

Actuellement, notre liste de données, notre FlatList, affiche une liste de films  this.state.films  . Lorsque vous aurez connecté le store Redux au component Search, vous aurez accès aux films favoris  this.props.favoritesFilm  .

Imaginons maintenant que vous ajoutiez un film aux favoris ; vous êtes d’accord avec moi pour dire que la liste des films favoris  this.props.favoritesFilm  va changer. Mais qu’en est-il de notre liste de films  this.state.films  ? Elle, elle ne change pas. Nous n’avons pas récupéré de nouveaux films de l’API au moment d’ajouter un film à nos favoris.

Du coup, lorsque vous allez ajouter un film aux favoris, le component Search va entrer dans le cycle de vie updating. Il va appeler son  render  , demander à notre FlatList de se re-rendre, mais cette dernière va répondre :

Oui, j’ai bien compris que l’on me demande de me re-rendre, MAIS mes données n’ont pas changé, donc je reste comme je suis et je ne me re-rends pas.

Ce qui, au niveau de notre application, fera que l’on ne re-rend jamais les items de notre FlatList (FilmItem) et donc que l’on n’affiche jamais l’image des favoris 🖤à côté du titre du film.

Donc la deuxième solution ne marche pas ? On repart sur la première solution ?

Attendez, attendez ! Je n’ai pas terminé. :D React Native propose une solution à notre problème. Cette solution, c’est la prop extraData d’une FlatList. Elle permet de dire à notre FlatList de se re-rendre si ses données et d’autres données (ajoutées dans extraData) ont changé.

Ici, lorsque vous aurez connecté le store Redux au component Search, vous pourrez ajouter la prop extraData sur la FlatList :

extraData={this.props.favoritesFilm}

Cela devrait beaucoup mieux fonctionner. ;)

Je ferme cette longue parenthèse sur le comportement des FlatList et vous laisse travailler. :p

Normalement, hormis la prop  extraData  , il n'y a pas de piège. Essayez de réaliser cette fonctionnalité par vous-même, cela vous permettra de manipuler Redux et son fonctionnement. Pour information, ici, pas de besoin de toucher au reducer ni au store, on a déjà tout ce qu'il nous faut pour faire cette fonctionnalité.

La solution :

// Components/Search.js
import React from 'react'
import { StyleSheet, View, TextInput, Button, Text, FlatList, ActivityIndicator } from 'react-native'
import FilmItem from './FilmItem'
import { getFilmsFromApiWithSearchedText } from '../API/TMDBApi'
import { connect } from 'react-redux'
class Search extends React.Component {
constructor(props) {
super(props)
this.searchedText = ""
this.page = 0
this.totalPages = 0
this.state = {
films: [],
isLoading: false
}
}
_loadFilms() {
if (this.searchedText.length > 0) {
this.setState({ isLoading: true })
getFilmsFromApiWithSearchedText(this.searchedText, this.page+1).then(data => {
this.page = data.page
this.totalPages = data.total_pages
this.setState({
films: [ ...this.state.films, ...data.results ],
isLoading: false
})
})
}
}
_searchTextInputChanged(text) {
this.searchedText = text
}
_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: [],
}, () => {
this._loadFilms()
})
}
_displayDetailForFilm = (idFilm) => {
console.log("Display film with id " + idFilm)
this.props.navigation.navigate("FilmDetail", { idFilm: idFilm })
}
_displayLoading() {
if (this.state.isLoading) {
return (
<View style={styles.loading_container}>
<ActivityIndicator size='large' />
</View>
)
}
}
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Rechercher' onPress={() => this._searchFilms()}/>
<FlatList
data={this.state.films}
extraData={this.props.favoritesFilm}
// On utilise la prop extraData pour indiquer à notre FlatList que d’autres données doivent être prises en compte si on lui demande de se re-rendre
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) =>
<FilmItem
film={item}
// Ajout d'une props isFilmFavorite pour indiquer à l'item d'afficher un 🖤 ou non
isFilmFavorite={(this.props.favoritesFilm.findIndex(film => film.id === item.id) !== -1) ? true : false}
displayDetailForFilm={this._displayDetailForFilm}
/>
}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) { // On vérifie également qu'on n'a pas atteint la fin de la pagination (totalPages) avant de charger plus d'éléments
this._loadFilms()
}
}}
/>
{this._displayLoading()}
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1
},
textinput: {
marginLeft: 5,
marginRight: 5,
height: 50,
borderColor: '#000000',
borderWidth: 1,
paddingLeft: 5
},
loading_container: {
position: 'absolute',
left: 0,
right: 0,
top: 100,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
})
// On connecte le store Redux, ainsi que les films favoris du state de notre application, à notre component Search
const mapStateToProps = state => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(Search)
// Components/FilmItem.js
import React from 'react'
import { StyleSheet, View, Text, Image, TouchableOpacity } from 'react-native'
import { getImageFromApi } from '../API/TMDBApi'
class FilmItem extends React.Component {
_displayFavoriteImage() {
if (this.props.isFilmFavorite) {
// Si la props isFilmFavorite vaut true, on affiche le 🖤
return (
<Image
style={styles.favorite_image}
source={require('../Images/ic_favorite.png')}
/>
)
}
}
render() {
const { film, displayDetailForFilm } = this.props
return (
<TouchableOpacity
style={styles.main_container}
onPress={() => displayDetailForFilm(film.id)}>
<Image
style={styles.image}
source={{uri: getImageFromApi(film.poster_path)}}
/>
<View style={styles.content_container}>
<View style={styles.header_container}>
{this._displayFavoriteImage()}
<Text style={styles.title_text}>{film.title}</Text>
<Text style={styles.vote_text}>{film.vote_average}</Text>
</View>
<View style={styles.description_container}>
<Text style={styles.description_text} numberOfLines={6}>{film.overview}</Text>
</View>
<View style={styles.date_container}>
<Text style={styles.date_text}>Sorti le 13/12/2017</Text>
</View>
</View>
</TouchableOpacity>
)
}
}
const styles = StyleSheet.create({
main_container: {
height: 190,
flexDirection: 'row'
},
image: {
width: 120,
height: 180,
margin: 5
},
content_container: {
flex: 1,
margin: 5
},
header_container: {
flex: 3,
flexDirection: 'row'
},
title_text: {
fontWeight: 'bold',
fontSize: 20,
flex: 1,
flexWrap: 'wrap',
paddingRight: 5
},
vote_text: {
fontWeight: 'bold',
fontSize: 26,
color: '#666666'
},
description_container: {
flex: 7
},
description_text: {
fontStyle: 'italic',
color: '#666666'
},
date_container: {
flex: 1
},
date_text: {
textAlign: 'right',
fontSize: 14
},
favorite_image: {
width: 25,
height: 25,
marginRight: 5
}
})
export default FilmItem

Alors, vous y êtes arrivé ? :)

Nous arrivons à la fin de ce chapitre et à la fin de Redux, on l'a fait. :pirate: Redux est indispensable lorsque l'on développe en React Native et plus globalement en React. L'architecture est un peu complexe et parfois un peu longue à mettre en place, mais elle est efficace. Et encore là, on n'a vu que la partie émergée de l'iceberg, Redux permettant d'aller beaucoup plus loin. Je vous remets le lien, si cela vous intéresse, pour suivre toutes les vidéos réalisées par l'un des créateurs de Redux. 

Ici, je vous ai parlé de Redux, car c'est la librairie la plus connue en React pour gérer des données de manière globale dans nos applications, mais il existe d'autres alternatives comme MobX et Relay. Pour information, Relay est créé et utilisé par Facebook.

C'est le moment de faire une pause bien méritée. Croyez-moi, vous venez de franchir une sacrée étape dans le développement sur React Native. :soleil:

Dans le prochain chapitre, nous allons créer une navigation plus avancée en utilisant le composant TabNavigator de la librairie React Navigation. On terminera le chapitre en créant une vue pour afficher les films favoris. Allez, quand vous êtes prêt, c'est parti ! Vous allez voir que les TabNavigators, à côté de Redux, cela se fait les yeux fermés.

À vous de jouer !

Avant de passer au chapitre suivant, je vous propose de créer votre Reducer avec Redux dans ce petit exercice ! :D C'est parti !

Console de code
Houston... ?
Il semblerait que votre ordinateur ne soit pas connecté à Internet.
Vous n'êtes pas connecté

Connectez-vous pour accéder aux exercices de codes et testez vos nouvelles compétences.

Pensez à vous entraîner avant de terminer ce chapitre.

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