• 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

Appréhendez le "setState"

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

Je ne sais pas si vous avez poussé vos tests de l'application très loin, mais si vous scrollez jusqu'en bas de la liste des films recherchés, vous devriez rapidement être bloqué, au bout de 20 films très exactement. C'est normal, l'API TMDB ne renvoie que 20 films par appel et gère ensuite une pagination pour récupérer les suivants.

Je vous propose de gérer une pagination dans notre liste de films. Dès que l'utilisateur arrive à la fin de notre liste de films, on va récupérer les films suivants et ainsi créer ce qu'on appelle en développement un scroll infini. Bien sûr, infini jusqu'à ce que l'API ne renvoie plus de films correspondant à votre recherche. :p Mais méfiez-vous, la base de  données  de TMDB est bien remplie.

Détectez la fin d'une FlatList

La première étape pour mettre en place un scroll infini est d'identifier l'évènement où l'utilisateur est arrivé au bout de notre FlatList. Si vous vous référez à la documentation d'une FlatList sur React Native, une fonction onReachEnd est plutôt prometteuse. En tout cas de nom, la description est un peu... complexe : 

Called once when the scroll position gets within  onEndReachedThreshold  of the rendered content.

Pour faire simple, l'évènement  onReachEnd  est appelé lorsque l'on a atteint une position définie par la valeur de  onEndReachedThreshold. Cette position se base sur le reste à afficher de votre FlatList. Prenons un exemple : 

Imaginons que notre FlatList fasse 100 pixels de haut. Le contenu de notre FlatList, avec tous ses items, représente une longueur de 500 pixels. Pour afficher toute notre FlatList, vous êtes d'accord qu'il faut scroller 500 pixels dans un espace de 100 pixels.

  • Si vous définissez onEndReachedThreshold  à 1, l'évènement  onReachEnd  se déclenchera lorsqu'il ne restera plus qu'une longueur de FlatList à afficher, c'est-à-dire 100 pixels à afficher. Notre évènement se déclenchera donc quand on aura scrollé 400 pixels dans notre FlatList (500-100). 

  • Si vous définissez onEndReachedThreshold  à 0.5, l'évènement  onReachEnd  se déclenchera lorsqu'il ne restera plus qu'une moitié de longueur de FlatList à afficher, c'est-à-dire 50 pixels à afficher. Notre évènement se déclenchera donc quand on aura scrollé 450 pixels dans notre FlatList (500-(100/2)). 

Finissons cet aparté qui n'engage que mon avis et passons à la récupération de l'évènement lorsque l'on atteint la fin de notre FlatList. On va définir  onEndReachedThreshold  à  0.5   pour que l'évènement  onReachEnd  se déclenche quand il ne reste plus qu'une moitié de longueur de notre FlatList à afficher. On va mettre un log pour vérifier que tout fonctionne.

// Components/Search.js
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
console.log("onEndReached")
}}
/>

Retournez côté application et rechargez votre application. Faites une recherche et scrollez jusqu'en bas de la liste de films. Regardez vos logs et constatez :

14:26:02: onEndReached

Super, cela fonctionne. :) On a notre évènement prêt, il ne reste plus qu'à charger les prochains films.

Gérez la pagination

Dans un chapitre précédent, on s'était intéressé aux retours de l'API TMDB en les affichant dans nos logs. Je vous invite cette fois à aller voir le résultat de l'API directement sur le web, afin d'avoir un rendu beaucoup plus visible. Vous allez comprendre pourquoi.

Sur un navigateur, accédez à l'URL https://api.themoviedb.org/3/search/movie?api_key=VOTRE_TOKEN_ICI&language=fr&query=Star en remplaçant bien VOTRE_TOKEN_ICI par votre token. Vous devriez voir le JSON retourné par l'API TMDB. Ce qui m'intéresse ici, ce sont les tout premiers champs : 

// http://api.themoviedb.org/3/search/movie?api_key=VOTRE_TOKEN_ICI&language=fr&query=Star
{
page: 1,
total_results: 1981,
total_pages: 100,
results: [
{...}
]
}

Vous voyez où je veux en venir ? ^^ On va utiliser les valeurs de  page  et  total_pages pour gérer notre pagination et, par la même occasion, notre scroll infini.

Dans le component Search, vous pouvez initialiser deux variables  page  et  totalPages . Elles nous seront utiles pour stocker les données retournées par l'API, respectivement  page  et  total_pages .

T'es gentil, mais je les mets où tes deux variables, dans le state ou en dehors ?

Bonne question. :) Quand nous définirons le numéro de la page courante et le nombre de pages, nous voudrons que notre component soit re-rendu ? Et, est-ce que nous allons utiliser et afficher ces deux informations dans le render du component Search ?

La réponse à ces deux questions est : non. De ce fait, nous n'allons pas utiliser le state ici, mais de simples variables de classe :

// Components/Search.js
constructor(props) {
super(props)
this.searchedText = ""
this.page = 0 // Compteur pour connaître la page courante
this.totalPages = 0 // Nombre de pages totales pour savoir si on a atteint la fin des retours de l'API TMDB
this.state = {
films: [],
isLoading: false
}
}

On va ensuite modifier notre fonction  getFilmsFromApiWithSearchedText  de l'API pour qu'elle prenne en compte la page courante. Sur l'API TMDB, on peut récupérer une page spécifique en passant le numéro de page dans les paramètres de l'URL :

// API/TMDBApi.js
export function getFilmsFromApiWithSearchedText (text, page) {
const url = 'https://api.themoviedb.org/3/search/movie?api_key=' + API_TOKEN + '&language=fr&query=' + text + "&page=" + page
return fetch(url)
.then((response) => response.json())
.catch((error) => console.error(error))
}

On a modifié la structure de la fonction  getFilmsFromApiWithSearchedText, il faut donc modifier l'appel que l'on fait actuellement dans  _loadFilms. On va également en profiter pour mettre à jour notre state avec les valeurs  page  et  total_pages  retournées par l'API : 

// Components/Search.js
_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
})
})
}
}

 Stop là, qu'est-ce que c'est que ça ?films: [ ...this.state.films, ...data.results ]  D'où sort tout ça ?

OK. Je vous avoue que là, c'est très particulier. :p

Si j'avais laissé  films: data.results , à chaque appel, on aurait écrasé et perdu les films que l'on a déjà récupérés. Notre but est d'ajouter les films à ceux que l'on a déjà récupérés et c'est exactement ce que l'on a fait ici. C'est une simplification permise par ES6, encore une fois. La syntaxe  ...tableau  crée une copie du tableau. Avec cette simplification, on doit passer deux copies de nos tableaux pour que la concaténation fonctionne. 

On est presque prêt. Il nous reste une dernière chose à faire : lancer la recherche des prochains films lorsque l'on atteint la fin de notre FlatList. Cela va aller vite, on a déjà préparé le terrain :

// Components/Search.js
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) { // On vérifie qu'on n'a pas atteint la fin de la pagination (totalPages) avant de charger plus d'éléments
this._loadFilms()
}
}}
/>

Cette fois, on est bon. Allez sur votre application, faites une recherche et scrollez, scrollez, scrollez. On y est arrivé, on a créé un scroll infini sur notre liste de films. :soleil: On peut découvrir des films que l'on n'a pas l'habitude de voir :

Scroll infini sur nos recherches
Scroll infini sur nos recherches

Si vous jouez un peu avec l'application, notamment si vous lancez plusieurs recherches, vous verrez que nos films s'empilent les uns après les autres. On a oublié de remettre à zéro les films de notre state après chaque recherche. Oups ! :lol:

Remettez le state à zéro

Pour différencier une recherche de films de l'appel d'une nouvelle page, on va créer une nouvelle fonction, spécialement pour la recherche. C'est ici que l'on remettra à 0 notre state. Enfin, plus spécifiquement, on remettra à zéro les films de notre state, juste avant d'appeler l'API :

// Components/Search.js
_searchFilms() {
// Ici on va remettre à zéro les films de notre state
this._loadFilms()
}

On va appeler cette fonction lorsqu'on lance une nouvelle recherche, c'est-à-dire à la validation du clavier et au clic sur le bouton "Rechercher" :

// Components/Search.js
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Rechercher' onPress={() => this._searchFilms()}/>

Notre fonction est créée, on l'appelle où il faut. Il ne nous reste plus qu'à écrire le code pour remettre à zéro les films de notre state et, vu que l'on n'est pas trop sûr de ce que l'on fait, on va ajouter un log juste après pour vérifier que nos films sont bien remis à zéro : :D

// Components/Search.js
_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: []
})
// J'utilise la paramètre length sur mon tableau de films pour vérifier qu'il y a bien 0 film
console.log("Page : " + this.page + " / TotalPages : " + this.totalPages + " / Nombre de films : " + this.state.films.length)
this._loadFilms()
}

Vous pouvez tester que cela fonctionne sur votre application. Faites une recherche, scrollez, faites une nouvelle recherche, scrollez, etc.

Côté application, cela fonctionne, notre liste de films est bien remise à zéro et on affiche les nouveaux films : 

Remise à zéro du state de films
Remise à zéro du state de films

C'est normal et, à vrai dire, je suis content que l'on ait rencontré ce problème. :) Vous risquez fortement de le rencontrer dans la réalisation de vos applications et, quand cela arrivera, vous serez préparé.  setState  est une fonction asynchrone

setState  Asynchrone

Async quoi ?

Asynchrone veut dire que c'est une fonction qui s'exécute en arrière-plan et qui ne bloque pas l'exécution de votre code. Cela signifie qu'ici, pendant que l'on remet le state à zéro, on est déjà en train d'afficher le log et d'appeler la fonction  _loadFilms() . 

Moi, cela ne me va pas. :p Dans notre application, je veux d'abord remettre le state et mes films à zéro, puis lancer une nouvelle recherche. Pour ce faire, on va se pencher sur la documentation de setState . 

setState(updater[, callback])

setState  possède un paramètre  callback  qui permet d'exécuter une action dès que notre state a fini de se mettre à jour. C'est parfait, on va utiliser ce paramètre pour lancer notre recherche une fois que le state a été remis à zéro : 

_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: [],
}, () => {
console.log("Page : " + this.page + " / TotalPages : " + this.totalPages + " / Nombre de films : " + this.state.films.length)
this._loadFilms()
})
}

Si vous testez à nouveau, vous verrez que notre application fonctionne toujours aussi bien. Ouf... :D Et côté logs, cela va beaucoup mieux :

09:20:34: Page : 0 / TotalPages : 0 / Nombre de films : 0
09:20:35: Page : 0 / TotalPages : 0 / Nombre de films : 0
09:20:36: Page : 0 / TotalPages : 0 / Nombre de films : 0
...

Cette fois, on lance bien notre recherche de nouveaux films  this.loadFilms()  seulement lorsque   setState  a fini de remettre à zéro nos films. 

On arrive à la fin de ce chapitre consacré à l'implémentation d'un scroll infini pour notre liste de films. Finalement, cette fois encore, on n'a pas découvert de nouveaux concepts en React, si ce n'est le fait que  setState  est asynchrone.

J'espère que ce chapitre et le précédent vous auront permis de consolider vos connaissances sur React Native.

Avant de passer à la suite et à la partie 3, je vous mets le code complet du fichier Search.js afin que l'on parte tous sur les mêmes bases pour la prochaine partie : 

// 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'
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()
})
}
_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}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) {
this._loadFilms()
}
}}
/>
{this._displayLoading()}
</View>
)
}
}
const styles = StyleSheet.create({
main_container: {
flex: 1,
marginTop: 20
},
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

Ce chapitre conclut la partie 2 de ce cours. Vous connaissez à présent les bases du développement en React Native. Si vous êtes arrivé jusqu'ici, vous êtes, comme moi, complètement séduit par cette technologie. On ne va pas s'arrêter en si bon chemin. ;)

Dans la prochaine partie, on va continuer d'améliorer notre application de gestion de films en ajoutant des vues. Vous apprendrez comment naviguer entre les vues, faire passer des données, etc. Mais avant, je vous propose un quiz sur tout ce que l'on vient de voir. Si vous vous sentez prêt, c'est parti ! :pirate:

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