• 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

Apprivoisez le Data Binding

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

Vous êtes encore là ? Les mots "data binding" ne vous ont pas fait fuir ? ^^ Tant mieux, c'est un concept hyper intéressant et vous allez comprendre énormément de choses sur le fonctionnement de vos applications.

Le data binding n'est pas un concept créé par React et Facebook, et c'est loin d'être nouveau. C'est un concept très utilisé dans le développement web. Si vous avez fait de l'AngularJS, par exemple, vous avez forcément dû en entendre parler et il y a fort à parier que je ne vous apprenne pas grand-chose ici.

Dans ce chapitre, je vais vous expliquer ce qu'est le data binding et comment il s'applique dans React. C'est un peu la compétence à débloquer pour corriger notre anomalie. Mais, avant de vous parler du data binding, il faut que je vous présente deux/trois spécificités du développement Javascript et notamment ce petit mot à 4 lettres : this , aussi appelé contexte.

Le contexte  this

Lorsque l'on développe en React, et plus généralement en Javascript, on utilise  this, le contexte, à tout bout de champ et parfois sans trop savoir pourquoi ni comment il fonctionne. 

Première chose très importante à savoir : le contexte  this  dépend de là où il est appelé. C'est assez basique comme affirmation, mais cela a son importance, vous allez voir. ;) Prenons l'exemple de ce code et intéressons-nous aux logs de  this.searchedText. Vous allez pouvoir vite faire le rapprochement avec notre erreur du chapitre précédent : (j'ai ajouté des numéros pour bien différencier les logs, je vous invite à tester, j'ai formaté le component exprès pour  :))

import React from 'react'
import { View } from 'react-native'
class MonComponent extends React.Component {
constructor(props) {
super(props)
this.searchedText = "Star wars"
}
_maFonction() {
console.log("Log 2 | searchText : " + this.searchedText)
}
render() {
console.log("Log 1 | searchText : " + this.searchedText)
this._maFonction()
return ( <View></View> )
}
}
export default MonComponent

Si vous testez ce code, vous allez voir dans votre console :

16:10:21: Log 1 | searchText : Star wars
16:10:21: Log 2 | searchText : Star wars

Décryptons ensemble ce qu'il se passe ici.

  1. Dans le render du component, on affiche  this.searchedText . À cet endroit, on se trouve dans le contexte du component MonComponent, donc  this  correspond au contexte du component MonComponent. C'est pour cela que le log 1 nous affiche bien la valeur  title  de MonComponent.

  2. Ensuite, on appelle la fonction  _maFonction()  en spécifiant bien  this._maFonction() . Cette spécification informe la fonction que l'on souhaite l'appeler et l'exécuter dans le contexte actuel et, le contexte actuel, c'est celui du component MonComponent. Le contexte  this  dans  _maFonction()  correspond donc au contexte du component MonComponent. C'est pour cela que le log 2 nous affiche bien la valeur  title  de MonComponent.

Ça va, vous suivez toujours ?

Maintenant, on va complexifier un peu notre exemple, on va modifier notre fonction  _maFonctionpour qu'elle prenne en paramètre et retourne une autre fonction. Inception:ninja: 

La fonction passée en paramètre et retournée est ce que l'on appelle un callback, ou, en français, fonction de rappel. Vous en avez peut-être déjà entendu parler.

Callback

Voici comment implémenter un callback en Javascript :

import React from 'react'
import { View } from 'react-native'
class MonComponent extends React.Component {
constructor(props) {
super(props)
this.searchedText = "Star wars"
}
_maFonction(functionCallback) {
console.log("Log 2 | searchedText : " + this.searchedText)
functionCallback()
}
_maFonctionATransmettre() {
console.log("Log 3 | searchedText : " + this.searchedText)
}
render() {
console.log("Log 1 | searchedText : " + this.searchedText)
this._maFonction(this._maFonctionATransmettre)
return ( <View></View> )
}
}
export default MonComponent

Si vous testez ce code, vous allez voir dans vos logs :

16:11:46: Log 1 | searchedText : Star wars
16:11:46: Log 2 | searchedText : Star wars
16:11:46: Log 3 | searchedText : undefined

Et si on fait un peu trop les fous :lol: en demandant la longueur du texte au niveau du log 3 :

_maFonctionATransmettre() {
console.log("Log 3 | searchedText : " + this.searchedText)
console.log("Log 3 | searchedText.length : " + this.searchedText.length)
}

Reprenons ce code et voyons ce qu'il se passe ici. Je repars du dernier point que l'on a vu.

On appelle la fonction  _maFonction()  en lui faisant passer en paramètre la fonction  _maFonctionATransmettre  et on lui indique de s'exécuter dans le contexte du component (this._maFonction() ). De ce fait, dans la fonction  _maFonction()  , quand vous utilisez  this , ligne 9, vous faites référence au contexte du component et le log fonctionne.

Ensuite, dans la fonction  _maFonction() , ligne 10, on appelle la fonction  functionCallback  , récupérée des paramètres de  _maFonction  . Mais, regardez bien, on l'appelle sans lui spécifier le contexte dans lequel on veut qu'elle s'exécute ! On n'a pas fait de  this.functionCallback()  . Du coup, quand on arrive dans l'exécution de la fonction, ligne 14, la fonction est perdue. On ne lui a pas dit dans quel contexte s'exécuter.

Quand c'est comme ça, la fonction ne s'embête pas, elle prend le contexte global de votre application, un contexte qui n'a rien à voir avec le contexte de votre component MonComponent. Le contexte global ne connaît pas la variable  searchedText  de MonComponent et, forcément, quand vous faites  this.searchedText.length  dans l'exécution de la fonction, log 3, votre application plante. :(

Alors, c'est bon, vous avez fait le rapprochement avec notre anomalie lorsque l'on charge plus de films ? Non ?

Allez, je vous aide un peu, je reprends mon exemple précédent en changeant juste le nom du component et les noms des fonctions. Cela devrait être un peu plus clair :

class Search extends React.Component {
constructor(props) {
super(props)
this.searchedText = "Star Wars"
}
filmList(loadFilms) {
console.log("Log 2 | title : " + this.searchedText)
loadFilms()
}
_loadFilms() {
console.log("Log 3 | title : " + this.searchedText)
if (this.searchedText.length > 0) {
//...
}
}
render() {
console.log("Log 1 | title : " + this.searchedText)
this.filmList(this._loadFilms)
return ( ... )
}
}

Bon, j'arrête avec mes devinettes et je vous montre du concret, promis. ;)

Dans notre application, lorsque l'on ajoute le component FilmList à notre component Search, on fait :

// Components/Search.js
class Search extends React.Component {
//...
render() {
return (
//...
<FilmList
//...
loadFilms={this._loadFilms}
/>
)
}
}

Ici, on crée un component FilmList et on lui fait passer, via ses props, la fonction  _loadFilms . C'est exactement ce que l'on fait ici : 

this._maFonction(this._maFonctionATransmettre)

Ensuite, côté component FilmList, on fait :

// Components/FilmList.js
class FilmList extends React.Component {
//...
render() {
return (
//...
<FlatList
//...
onEndReached={() => {
//...
this.props.loadFilms()
}}
/>
)
}
}

Ici, on appelle la fonction récupérée via les props. C'est notre callback, il est là. :) Et cela revient à ce que l'on a fait ici : 

functionCallback()

Quoique pas tout à fait ! On appelle la fonction en faisant  this.props.loadFilms() . Vous devez être en train de vous dire que l'on indique à notre callback  loadFilms  de s'exécuter dans le contexte des props du component FilmList... Et bien, vous avez parfaitement raison, c'est exactement ce qu'il se passe ici. On exécute le callback dans le contexte des props du component.

Donc de retour dans la fonction  _loadFilms  du component Search, après l'appel du callback : 

// Components/Search.js
class Search extends React.Component {
_loadFilms() {
if (this.searchedText.length > 0) { ... }
}
}

this correspond au contexte des props du component FilmList. C'est fou, non ?  :waw: Rien à voir avec le contexte du component Search. Ce contexte ne connaît pas  this.searchedText  et  this.searchedText.length  fait planter notre application ! En fait, ici, c'est comme si on avait fait, dans le component FilmList,  this.props.searchedText.length  . Cela devient de plus en plus clair ?

La preuve par A + B

Bien sûr, vous n'êtes pas obligé de me croire sur parole quand je vous dis qu'une fois le callback appelé,  this  correspond au contexte des props de FilmList. Je vous ai donc préparé un petit exemple, vous allez pouvoir vérifier vous-même. ;) Dans la création du component FilmList, on va faire passer une nouvelle prop test  :

// Components/Search.js
<FilmList
films={this.state.films}
navigation={this.props.navigation}
loadFilms={this._loadFilms}
page={this.page}
totalPages={this.totalPages}
test={"props du component FilmList"}
/>

Puis on va ajouter un log dans la fonction  _loadFilms()  pour vérifier ce que contient le contexte  this  une fois le callback  loadFilms  appelé :

// Components/Search.js
_loadFilms() {
console.log("Contenu de test : " + this.test)
if (this.state.searchedText.length > 0) {
//...
}
}

Placez-vous sur votre application et observez bien les logs. Commencez par chercher un film, vous devriez voir apparaître dans les logs :

17:39:19: Contenu de test : undefined

C'est normal, lors d'une recherche de film, on appelle la fonction  loadFilms  depuis le contexte du component Search et ce dernier ne connaît pas  test  . Maintenant, scrollez dans votre liste de films jusqu'à déclencher l'appel du callback pour charger plus de films et là : 

17:39:21: Contenu de test : props du component FilmList

Alors, vous voyez ? Cette fois, on se trouve bien dans le contexte des props du component FilmList et lui, il connaît  test . C'est bluffant ! :soleil: Vous pouvez avoir une fonction rodée, qui tourne et qui fait son job. Si vous l'appelez dans un contexte différent, plus rien ne marche. C'est le genre d'erreur qui ne saute pas aux yeux tout de suite et que l'on rencontre en ce moment dans notre application.

Bon, maintenant que je vous ai assommé avec tous ces paragraphes pour vous expliquer pourquoi notre application ne marche pas, il serait peut-être temps de regarder comment la faire marcher. La solution se trouve dans le titre de ce chapitre, ce pour quoi on est là, à la base : le data binding

Data Binding, c'est quoi ?

Le data binding est l'association des données d'un élément à un autre élément. Pour faire simple, si vous avez un élément A avec 10 films et que vous bindez ses 10 films avec un élément B, l'élément B aura lui aussi les 10 films. Si vous supprimez un film de l'élément A, l'élément A et l'élément B n'auront plus que 9 films. En fait, avec le data binding, dans l'élément B, vous accédez aux films de l'élément A : 

Schéma du Data Binding
Schéma du Data Binding

C'est ça, le principe du data binding : on associe des données à d'autres éléments.

OK, j'ai compris, mais on en fait quoi, de ton data binding ?

Si je vous dis que l'on peut binder le contexte  this  d'un élément à un autre élément, là, cela vous intéresse. :) La solution à notre anomalie est là. Nous allons binder le contexte du component Search à notre fonction  loadFilms  : 

Schéma du bind du contexte du component Search à la fonction loadFilms
Schéma du bind du contexte du component Search à la fonction loadFilms

Côté code, binder un élément à un autre passe par l'utilisation de la fonction Javascript bind. Dans le constructeur du component Search, faites ceci : 

// Components/Search.js
class Search extends React.Component {
constructor(props) {
super(props)
//...
this._loadFilms = this._loadFilms.bind(this)
}
// ...
}

Oui, la syntaxe est... particulière, mais elle est ce qu'elle est. ;) Concrètement, ici, on bind  this , le contexte du component Search, à la fonction  _loadFilms . On bind des données d'un élément à un autre, c'est du data binding. Votre fonction devient une fonction bindée ou, en anglais, bounded function.

Vous avez associé le contexte du component Search à votre fonction  _loadFilms . À présent, et quel que soit l'endroit depuis lequel vous appelez la fonction  _loadFilms  , si vous utilisez  this  dans la fonction  _loadFilms  , vous faites toujours référence au contexte du component Search.

Ce qui signifie que :

  • si vous appelez la fonction  _loadFilms  depuis le contexte du component Search,  dans la fonction  _loadFilms  , this  correspondra au contexte du component Search ;

  • si vous appelez la fonction  _loadFilms  depuis le contexte des props du component FilmList, comme avec notre callback, dans la fonction  _loadFilms  , this  correspondra au contexte du component Search.

Vous pouvez tester dans votre application que cela fonctionne avec le  bind  . Faites une recherche, puis scrollez jusqu'en bas de la liste de films pour déclencher le callback et charger de nouveaux films. Ça marche ! Terminé les plantages. :magicien:

Vous voyez que cette anomalie n'était pas si simple ! Bon, OK, elle est rapidement corrigée, mais avant d'appliquer un bout de code, il faut comprendre pourquoi on a besoin de le faire.

Dis donc, tu n'essaies pas un peu de nous la faire à l'envers, là ? :colere: Dans un chapitre précédent, on a bien transmis une fonction  _displayDetailForFilm  à un component enfant FilmItem pour afficher le détail d'un film. Puis, on l'a appelée, via un callback, et bien sûr sans avoir bindé la fonction. On n'a pas eu de crash à ce moment-là, comment cela se fait-il ?

Bien vu, et vous avez tout à fait raison. C'est parce que j'ai utilisé une des syntaxes permises par ES6 : les fonctions fléchées.

Fonctions fléchées

Je vous remets le code en question et essayez de trouver la différence qui pourrait faire que cela ne plantait pas :

  1. On transmet la fonction du component parent FilmList au component enfant FilmItem.

    // Components/FilmList.js
    <FilmItem
    //...
    displayDetailForFilm={this._displayDetailForFilm}
    />
  2. On appelle la fonction dans le component enfant, callback.

    // Components/FilmItem.js
    <TouchableOpacity
    onPress={() => this.props.displayDetailForFilm(film.id)}>
    // Dans le code on a :
    // const { film, displayDetailForFilm } = this.props
    // onPress={() => displayDetailForFilm(film.id)}>
    // C'est la même chose que mon code dans le onPress
  3. Enfin, dans la fonction _displayDetailForFilm  du component parent FilmList, on accède aux  props  du component FilmList.

    // Components/FilmList.js
    _displayDetailForFilm = (idFilm) => {
    this.props.navigation.navigate('FilmDetail', {idFilm: idFilm})
    }

Avec ce que l'on a vu dans ce chapitre, normalement, ici, cela devrait planter, puisque le contexte  this  devrait être celui des props du component FilmItem et que ce dernier ne connaît pas  this.props.navigation . Mais cela ne plante pas, pourquoi ?

Tout simplement parce que j'ai un peu triché, désolé. :'( Je vous ai caché une information importante lorsque l'on a créé la fonction  _displayDetailForFilm . Vous voyez sa syntaxe de déclaration :  _displayDetailForFilm = (idFilm) => {} ? C'est ce que l'on appelle une fonction fléchée (arrow function) et, en plus d'être jolie, elle a une particularité.

Quand vous créez une fonction fléchée, celle-ci est automatiquement bindée avec le contexte qui l'englobe. Dans notre application, la fonction est créée dans le component FilmList, elle est donc automatiquement bindée avec le contexte du component FilmList.

C'est pour cela que  this.props.navigation  existe et permet à l'application de naviguer vers la vue FilmDetail. C'est exactement comme si on avait, dans le component FilmList : 

// Components/FilmList.js
class FilmList extends React.Component {
constructor(props) {
super(props)
//...
this._displayDetailForFilm = this._displayDetailForFilm.bind(this)
}
_displayDetailForFilm(idFilm) {
//...
}
//...
}

Quelle solution choisir pour binder ses données ?

Celle que vous voulez. ;)Je vous ai présenté deux options pour binder vos données, libre à vous de choisir votre préférée. Il existe d'autres solutions, moins utilisées, que je ne présenterai pas ici, mais si cela vous intéresse, il y a un article très bien fait à ce sujet : React Binding Patterns: 5 Approaches for Handling `this`.

Je ferme cette longue parenthèse sur le data binding en React et plus généralement en Javascript. Quelle que soit la solution que vous avez utilisée pour binder la fonction, votre application est censée fonctionner à présent. Pour terminer notre chapitre, il nous reste à construire la vue Favoris et, avec toutes les préparations que l'on vient de faire, cela va aller vite. :pirate:

Vue Favoris

Je vous invite à essayer par vous-même avant de regarder la solution. Vraiment, ici, il n'y a pas grand-chose à faire. Pour vous aider, j'ai préparé une petite liste des points à ne pas oublier :

  1. Connecter le store Redux au component Favorites et transmettre au component FilmList les films favoris du state de l'application.

  2. Créer et ajouter un StackNavigator pour l'onglet favoris.

  3. Bien différencier le cas où l'on affiche la liste de films depuis l'onglet recherche et depuis l'onglet favoris. Quand on arrive à la fin de la liste des films favoris, il ne faut pas appeler la fonction   loadFilms . Personnellement, j'ai utilisé un booléen  favoriteList  au moment de la déclaration du component FilmList pour différencier les deux cas.

  4. (Bonus) Les détails des films favoris sont déjà stockés dans notre state global. De ce fait, quand on arrive sur la vue détail d'un film favori, ce n'est pas nécessaire de refaire un appel API, on peut utiliser le détail du film stocké.

Allez, à vous de jouer. ;)

Solution :

// Navigation/Navigations.js
import React from 'react'
import { StyleSheet, Image } from 'react-native'
import { createStackNavigator, createBottomTabNavigator, createAppContainer } from 'react-navigation'
import Search from '../Components/Search'
import FilmDetail from '../Components/FilmDetail'
import Favorites from '../Components/Favorites'
const SearchStackNavigator = createStackNavigator({
Search: {
screen: Search,
navigationOptions: {
title: 'Rechercher'
}
},
FilmDetail: {
screen: FilmDetail
}
})
const FavoritesStackNavigator = createStackNavigator({
Favorites: {
screen: Favorites,
navigationOptions: {
title: 'Favoris'
}
},
FilmDetail: {
screen: FilmDetail
}
})
const MoviesTabNavigator = createBottomTabNavigator(
{
Search: {
screen: SearchStackNavigator,
navigationOptions: {
tabBarIcon: () => {
return <Image
source={require('../Images/ic_search.png')}
style={styles.icon}/>
}
}
},
Favorites: {
screen: FavoritesStackNavigator,
navigationOptions: {
tabBarIcon: () => {
return <Image
source={require('../Images/ic_favorite.png')}
style={styles.icon}/>
}
}
}
},
{
tabBarOptions: {
activeBackgroundColor: '#DDDDDD',
inactiveBackgroundColor: '#FFFFFF',
showLabel: false,
showIcon: true
}
}
)
const styles = StyleSheet.create({
icon: {
width: 30,
height: 30
}
})
export default createAppContainer(MoviesTabNavigator)
// Components/Search.js
import React from 'react'
import { StyleSheet, View, TextInput, Button, Text, FlatList, ActivityIndicator } from 'react-native'
import FilmItem from './FilmItem'
import FilmList from './FilmList'
import { getFilmsFromApiWithSearchedText } from '../API/TMDBApi'
class Search extends React.Component {
constructor(props) {
super(props)
this.searchedText = ""
this.page = 0
this.totalPages = 0
this.state = {
films: [],
isLoading: false
}
this._loadFilms = this._loadFilms.bind(this)
}
_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()}/>
<FilmList
films={this.state.films}
navigation={this.props.navigation}
loadFilms={this._loadFilms}
page={this.page}
totalPages={this.totalPages}
favoriteList={false} // Ici j'ai simplement ajouté un booléen à false pour indiquer qu'on n'est pas dans le cas de l'affichage de la liste des films favoris. Et ainsi pouvoir déclencher le chargement de plus de films lorsque l'utilisateur scrolle.
/>
{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'
}
})
export default Search
// Components/Favorites.js
import React from 'react'
import { StyleSheet, Text } from 'react-native'
import FilmList from './FilmList'
import { connect } from 'react-redux'
class Favorites extends React.Component {
render() {
return (
<FilmList
films={this.props.favoritesFilm}
navigation={this.props.navigation}
favoriteList={true} // Ici on est bien dans le cas de la liste des films favoris. Ce booléen à true permettra d'empêcher de lancer la recherche de plus de films après un scroll lorsqu'on est sur la vue Favoris.
/>
)
}
}
const styles = StyleSheet.create({})
const mapStateToProps = state => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(Favorites)
// Components/FilmList.js
import React from 'react'
import { StyleSheet, FlatList } from 'react-native'
import FilmItem from './FilmItem'
import { connect } from 'react-redux'
class FilmList extends React.Component {
constructor(props) {
super(props)
this.state = {
films: []
}
}
_displayDetailForFilm = (idFilm) => {
console.log("Display film " + idFilm)
this.props.navigation.navigate('FilmDetail', {idFilm: idFilm})
}
render() {
return (
<FlatList
style={styles.list}
data={this.props.films}
extraData={this.props.favoritesFilm}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => (
<FilmItem
film={item}
isFilmFavorite={(this.props.favoritesFilm.findIndex(film => film.id === item.id) !== -1) ? true : false} // Bonus pour différencier les films déjà présent dans notre state global et qui n'ont donc pas besoin d'être récupérés depuis l'API
displayDetailForFilm={this._displayDetailForFilm}
/>
)}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (!this.props.favoriteList && this.props.page < this.props.totalPages) {
this.props.loadFilms()
}
}}
/>
)
}
}
const styles = StyleSheet.create({
list: {
flex: 1
}
})
const mapStateToProps = state => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmList)
// Components/FilmDetail.js
import React from 'react'
import { StyleSheet, View, Text, ActivityIndicator, ScrollView, Image, TouchableOpacity } 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: false
}
}
componentDidMount() {
const favoriteFilmIndex = this.props.favoritesFilm.findIndex(item => item.id === this.props.navigation.state.params.idFilm)
if (favoriteFilmIndex !== -1) { // Film déjà dans nos favoris, on a déjà son détail
// Pas besoin d'appeler l'API ici, on ajoute le détail stocké dans notre state global au state de notre component
this.setState({
film: this.props.favoritesFilm[favoriteFilmIndex]
})
return
}
// Le film n'est pas dans nos favoris, on n'a pas son détail
// On appelle l'API pour récupérer son détail
this.setState({ isLoading: true })
getFilmDetailFromApi(this.props.navigation.state.params.idFilm).then(data => {
this.setState({
film: data,
isLoading: false
})
})
}
_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)
}
_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}
/>
)
}
_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>
<TouchableOpacity
style={styles.favorite_container}
onPress={() => this._toggleFavorite()}>
{this._displayFavoriteImage()}
</TouchableOpacity>
<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'
},
favorite_container: {
alignItems: 'center',
},
description_text: {
fontStyle: 'italic',
color: '#666666',
margin: 5,
marginBottom: 15
},
default_text: {
marginLeft: 5,
marginRight: 5,
marginTop: 5,
},
favorite_image: {
width: 40,
height: 40
}
})
const mapStateToProps = (state) => {
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmDetail)

Côté application, vous devriez avoir comme prévu :

Vue Favoris et détail d'un film favoris
Vue Favoris et détail d'un film favori

Alors, vous y êtes arrivé ? Chapitre pas facile, je vous l'accorde, mais il fallait que l'on y passe. Dans mes souvenirs, j'ai passé énormément de temps sur une anomalie similaire. Il m'a fallu plusieurs heures pour corriger l'anomalie et plusieurs autres pour comprendre pourquoi. Vu la taille du chapitre, il faut croire que cela m'a marqué. ^^

Même si c'est un comportement lié à Javascript, cela nous a permis de voir plus en profondeur le fonctionnement des components en React. Il faut savoir que React respecte à la lettre le fonctionnement de Javascript. Si Javascript vous oblige à binder votre fonction, React ne dira pas le contraire.

Nous arrivons à la fin de la partie 3. Notre application avance bien, vraiment bien. ;) Je vous avoue que les chapitres n'étaient pas faciles. On a vu des notions complexes du développement en React Native, mais bravo, vous avez tenu le coup. Vous êtes en mesure de créer des applications vraiment poussées à présent. Vous savez comment créer une navigation plus ou moins complexe avec la librairie React Navigation et comment faire passer des informations d'une vue à l'autre.

On a également vu comment créer un state global dans nos applications avec Redux et gérer des données de manière globale. Redux reste une librairie complexe à appréhender et encore, nous n'avons vu qu'une partie de toute sa puissance. Je vous montrerai d'autres fonctionnalités possibles avec Redux dans la 4e et dernière partie. 

D'ailleurs, en parlant de cette dernière partie, on va se recentrer sur le développement purement en React Native. Fini les librairies externes, comme React Navigation et Redux. Au menu, on va voir ensemble les développements spécifiques, les animations, l'utilisation des composants du téléphone, le débugage poussé d'application, la livraison sur les stores, etc. En bref, c'est une panoplie d'outils qui nous sera très utile pour finaliser notre application. Mais avant cela, un quiz sur ce que l'on a vu en partie 3.

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