Thibault Mocellin

Thibault Mocellin

Développeur Full-Stack freelance basé à Annecy 🇫🇷

React Native : Utilisation de FlatList et SectionList

Posted on September 19, 2017

Dans cet article nous allons voir comment utiliser les composants FlatList et SectionList dans React Native. FlatList et SectionList sont deux composants qui vont nous permettre d’afficher sous forme de liste un ensemble de données. Jusqu’à la version 0.43 de React Native le composant pour afficher des données sous forme de liste était la ListView.

Cependant ListView est maintenant deprecated au profit de FlatList et SectionList qui sont plus simples à utiliser et aussi plus performants.

Pour illustrer l’utilisation de ces composants nous allons nous servir de l’API RandomUser.

Commençons par créer le projet

react-native init discoverFlatAndSectionList
cd discoverFlatAndSectionList/ && mkdir src

Ensuite nous allons créer un fichier index.js dans le dossier src et ajouter le code suivant :

import React, { Component } from 'react'
import { AppRegistry, StyleSheet, Text, View } from 'react-native'

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}> Welcome to React Native! </Text>
        <Text style={styles.instructions}>To get started, edit index.ios.js</Text>

        <Text style={styles.instructions}>Press Cmd+R to reload,{'\n'} Cmd+D or shake for dev menu</Text>
      </View>
    )
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: { fontSize: 20, textAlign: 'center', margin: 10 },
  instructions: { textAlign: 'center', color: '#333333', marginBottom: 5 },
})

Enfin on remplace le code des fichiers index.android.js et index.ios.js par le code suivant :

import React, { Component } from 'react'
import { AppRegistry } from 'react-native'
import App from './src'

AppRegistry.registerComponent('discoverFlatAndSectionList', () => App)

FlatList

Nous allons d’abord commencer par la FlatList puis nous verrons la SectionList.

Dans le dossier src créez un dossier components, à l’intérieur de ce dossier créez un fichier UserList.js.

Ensuite ajoutez-y le code suivant :

import React from 'react'
import { FlatList, Text } from 'react-native'

const _renderItem = ({ item }) => <Text>{item.email}</Text>

export default UserList = props => <FlatList data={props.data} renderItem={_renderItem} />

Revenons en détail sur composant :

  1. On importe React ainsi que les composants FlatList et Text de ReactNative
  2. On ajoute la fonction _renderItem qui va nous permettre de définir comment sera rendu notre item dans la liste pour le moment on affiche juste l’email de chaque utilisateur
  3. On définit notre FlatList
<FlatList data={props.data} renderItem={_renderItem} />

Les propriétés du composant FlatList sont les suivantes :

  • data : C’est la source des données de la liste, il s’agit d’un tableaux
  • renderItem : C’est la fonction qui permet de définir comment afficher les éléments

Ensuite pour pouvoir afficher la liste on remplace le code du fichier index.js par le suivant :

import React, { Component } from 'react'
import { StyleSheet, View } from 'react-native'
import UserList from './components/UserList'

const sampleData = [
  {
    name: { title: 'mr', first: 'karl', last: 'johnson' },
    email: 'karl.johnson@example.com',
    picture: {
      thumbnail: 'https://randomuser.me/api/portraits/thumb/men/62.jpg',
    },
  },
  {
    name: { title: 'mrs', first: 'asuncion', last: 'gomez' },
    email: 'asuncion.gomez@example.com',
    picture: {
      thumbnail: 'https://randomuser.me/api/portraits/thumb/women/52.jpg',
    },
    nat: 'ES',
  },
  {
    name: { title: 'miss', first: 'gilcenira', last: 'ribeiro' },
    email: 'gilcenira.ribeiro@example.com',
    picture: {
      thumbnail: 'https://randomuser.me/api/portraits/thumb/women/21.jpg',
    },
  },
]

export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <UserList data={sampleData} />
      </View>
    )
  }
}
const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 20 } })

Dans un premier temps nous n’utiliserons pas l’API de RandomUser afin de simplifier les choses.

Vous devriez obtenir ce résultat :

react native flatlist

Vous remarquerez le message d’erreur en bas de l’écran. Il nous avertit que les éléments de la liste n’ont pas de clés. Le composant VirtualizedList (sur lequel sont basés les composants FlatList et SectionList) à besoin d’un identifiant unique pour chaque élément afin d’améliorer les performances des listes.

Pour palier à ça la FlatList dispose d’un propriété nommée KeyExtractor qui permet de définir quel attribut de l’élément de la liste doit être utilisé comme clé.

<FlatList data={props.data} renderItem={_renderItem} keyExtractor={item => item.email} />

Nous allons maintenant créer le composant qui sera utilisé dans la méthode renderItem. Toujours dans le dossier components créez un fichier nommé UserRow.js et ajoutez le code ci-dessous :

import React from 'react'
import { View, Text, Image, StyleSheet } from 'react-native'

export default UserRow = props => (
  <View style={styles.row}>
    <Image style={styles.picture} source={{ uri: props.picture }} />
    <View>
      <Text style={styles.primaryText}>{props.name + ' ' + props.firstName}</Text>
      <Text style={styles.secondaryText}>{props.email}</Text>
    </View>
  </View>
)

const styles = StyleSheet.create({
  row: { flexDirection: 'row', alignItems: 'center', padding: 12 },
  picture: { width: 50, height: 50, borderRadius: 25, marginRight: 18 },
  primaryText: {
    fontWeight: 'bold',
    fontSize: 14,
    color: 'black',
    marginBottom: 4,
  },
  secondaryText: { color: 'grey' },
})

Ensuite il ne reste plus qu’a importer ce composant dans le composant UserList et mettre à jour la méthode _renderItem :

import UserRow from './UserRow'

const _renderItem = ({ item }) => (
  <UserRow name={item.name.last} firstName={item.name.first} picture={item.picture.thumbnail} email={item.email} />
)

Voici le résultat : react native flatlist

Ici on remarque que nous n’avons pas de séparateur entre chaque ligne. Si nous souhaitons en ajouter nous avons deux possibilités.

  1. Ajouter une bordure en bas de la View de notre composant UserRow
  2. Utiliser une propriété de la FlatList

Nous allons voir comment utiliser la deuxième solution. La FlatList possède une propriété nommée ItemSeparatorComponent qui fonctionnement sensiblement de la même manière que la propriété renderItem.

Nous allons créer une fonction nommée _renderSeparator qui retournera le composant que l’on souhaite utilisé comme séparateur, dans notre cas ce sera simple une View avec quelques propriétés de style :

const _renderSeparator = () => <View style={{ height: 1, backgroundColor: 'grey', marginLeft: 80 }} />

Ensuite on ajoute la propriété ItemSeparatorComponent à notre FlatList

<FlatList
  data={props.data}
  renderItem={_renderItem}
  keyExtractor={item => item.email}
  ItemSeparatorComponent={_renderSeparator}
/>

Header et Footer

Header et Footer Nous pouvons ajouter à notre liste un composant Header (souvent utilisé pour le barre de recherche) ainsi qu’un composant Footer.

Pour commencer nous allons créer deux fonctions _renderHeader et renderFooter :

const _renderHeader = () => (
  <View style={{ height: 30, backgroundColor: '#4fc3f7', justifyContent: 'center' }}>
    <Text>Header</Text>
  </View>
)
const _renderHeader = () => (
  <View style={{ height: 30, backgroundColor: '#4fc3f7', justifyContent: 'center' }}>
    <Text>Footer</Text>
  </View>
)

Ici les composants affichés dans le header et le footer n’ont aucun intérêt, le but est de juste analyser le fonctionnement. Ensuite on ajoute les propriétés ListHeaderComponent et ListFooterComponent à notre composant FlatList :

<FlatList
  data={props.data}
  renderItem={_renderItem}
  keyExtractor={item => item.email}
  ItemSeparatorComponent={_renderSeparator}
  ListHeaderComponent={_renderHeader}
  ListFooterComponent={_renderFooter}
/>

EmptyComponent

Il est aussi possible de définir le composant à afficher lorsque la liste des données est vide pour ce faire nous devons utiliser la propriété ListEmptyComponent et définir la fonction _renderEmpty.

const _renderEmpty = () => (
  <View style={{ height: 40, alignItems: "center", justifyContent: "center" }}>
    <Text>Aucun résultat</Text>
  </View>
);

<FlatList
  data={props.data}
  ...
  ListEmptyComponent={_renderEmpty}  />

Récupération des données de l’api RandomUser

Avant de passer à la suite des fonctionnalités de FlatList nous allons mettre en place la récupération des données de l’api RandomUser.

Dans le fichier index.js la première chose que nous allons faire est de définir notre State

state = {
  page: 1,
  results: 20,
  totalPage: 3,
  seed: 'demo',
  isFetching: false,
  data: [],
}
  • page : Page que l’on veut récupérer
  • results : Nombre de résultat que l’on souhaite par page
  • totalPage : Nombre total de page
  • seed : L’api génère des résultats aléatoires la variable seed permet de récupérer toujours les même résultats
  • isFetching : Indique si l’on est entrain de récupérer les résultats depuis l’api
  • data : Les résultats renvoyés par l’api

Ensuite nous allons créer une fonction qui retournera les données et une méthode qui mettra à jour le state :

async fetchData(page) {
  const uri = "https://randomuser.me/api/";
  const response = await fetch(`${uri}?page=${page}&results=${this.state.results}&seeds=${this.state.seed}`);
  const jsondata = await response.json();
  return jsondata.results;
}

async loadData(page) {
  this.setState({ isFetching: true });
  const data = await this.fetchData(page);
  const nextPage = page + 1;
  this.setState({page: nextPage,data: [...this.state.data, ...data],isFetching: false,});
}

Enfin il ne nous reste plus qu’a récupérer les données dès que le composant est monté.

async componentDidMount() {
   await this.loadData(this.state.page);
}

Maintenant que nos données se chargent correctement nous allons remplacer le composant du footer actuelle afin d’avoir un indicateur lorsque les données sont en train de se charger.

Remplacez la fonction _renderFooter du fichier UserList.js par celle ci-dessous :

const _renderFooter = isFetching => {
  if (isFetching) {
    return <ActivityIndicator size="large" animating={true} color="#4fc3f7" style={{ marginBottom: 12 }} />
  }
  return null
}

Ensuite il faut modifier le composant FlatList de la manière suivante :

<FlatList
  data={props.data}
  renderItem={_renderItem}
  keyExtractor={item => item.email}
  ItemSeparatorComponent={_renderSeparator}
  ListHeaderComponent={_renderHeader}
  ListFooterComponent={() => _renderFooter(props.isFetching)}
  ListEmptyComponent={_renderEmpty}
/>

Ici le composant s’attend à avoir dans la liste de ses propriétés une propriété isFetching cet propriété aura pour valeur la valeur de l’attribut isFetching du state de l’application. Pour cela il ne reste plus qu’a passer la propriété au composant UserList dans le fichier index.js.

<UserList data={this.state.data} isFetching={this.state.isFetching} />

Pagination

Actuellement nos données se chargent correctement mais seulement la première page de résultat est chargée. Nous allons rajouter la pagination à notre liste. Le principe de fonctionnement sera le suivant on affichera un bouton dans le footer de la liste lorsque des résultats supplémentaires seront disponibles et on passera à notre composant UserList une propriété loadMore qui sera utilisée lorsqu’on clique sur le bouton pour charger plus de résultat et une propriété hasMoreResult pour savoir si l’on doit afficher le bouton ou non.

Pour commencer nous allons rajouter dans le state l’attribut hasMoreResult qui sera passer en tant que propriété au composant UserList.

Dans le fichier index.js ajouter au state l’attribut hasMoreResult :

state = {
  page: 1,
  results: 20,
  totalPage: 3,
  seed: 'demo',
  isFetching: true,
  data: [],
  hasMoreResult: true,
}

Ensuite dans la méthode loadData il faut mettre à jour la valeur de hasMoreResult :

this.setState({
  page: nextPage,
  data: [...this.state.data, ...data],
  isFetching: false,
  hasMoreResult: nextPage <= this.state.totalPage,
})

Ensuite on passe les propriétés au composant UserList :

<UserList
  data={this.state.data}
  isFetching={this.state.isFetching}
  loadMore={() => this.loadData(this.state.page)}
  hasMoreResult={this.state.hasMoreResult}
/>

Maintenant nous allons modifier la fonction _renderFooter dans le fichier UserList.js.

const _renderFooter = (isFetching, hasMoreResult, loadMore) => {
  if (isFetching) {
    return <ActivityIndicator size="large" animating={true} color="#4fc3f7" style={{ marginBottom: 12 }} />
  }
  if (hasMoreResult) {
    return <Button color="#4fc3f7" title="Afficher plus" onPress={loadMore} />
  }
  return null
}

Ici nous avons rajouter les deux paramètres hasMoreResult et loadMore à notre fonction, ensuite on teste si il y a encore des résultats à charger et on retourne un bouton avec comme action LoadMore.

Enfin il faut modifier la propriété ListFooterComponent de la FlatList de la manière suivante :

ListFooterComponent={() => _renderFooter(props.isFetching, props.hasMoreResult, props.loadMore)}

Pull To Refresh

Pour utiliser le pull to refresh sur la FlatList il faut d’abord importer le composant RefreshControl depuis ReactNative et ensuite l’ajouter à notre FlatList.

Dans le fichier UserList.js importez le composant RefreshControl :

import { FlatList, Text, View, ActivityIndicator, Button, RefreshControl } from 'react-native'

Ensuite ajoutez le à la FlatList :

refreshControl={
  <RefreshControl refreshing={props.refreshing} onRefresh={props.refresh} />
}

Le composant RefreshControl à besoin de deux propriétés :

  • refreshing : Booléen qui indique si l’on est en train de rafraichir les données identique en terme de fonctionnement à isFetching précédemment utilisé.
  • refresh : La fonction à executer pour rafraichir les données.

Dans notre cas ces deux propriétés vont être récupérer via les propriétés de notre composant UserList.

Nous allons donc maintenant modifier le fichier index.js pour passer les propriétés manquantes.

On commence par modifier le state pour ajouter l’attribut refreshing :

state = {
  page: 1,
  results: 20,
  totalPage: 3,
  seed: 'demo',
  isFetching: false,
  data: [],
  hasMoreResult: true,
  refreshing: false,
}

Ensuite nous allons créer la méthode refreshData :

async refreshData() {
this.setState({ refreshing: true });
const data = await this.fetchData(1);
this.setState({page: 2,data: data,refreshing: false,hasMoreResult: true});
}

Cette méthode est sensiblement identique à la méthode loadData cependant ici on passe directement 1 comme valeur pour la page et on met à jour la valeur refreshing à la place de isFetching.

Il ne nous reste plus qu’a passer les propriétés au composant UserList :

<UserList
  data={this.state.data}
  isFetching={this.state.isFetching}
  loadMore={() => this.loadData(this.state.page)}
  hasMoreResult={this.state.hasMoreResult}
  refreshing={this.state.refreshing}
  refresh={() => this.refreshData()}
/>

SectionList

Nous allons maintenant passé au composant SectionList, ce composant va nous permettre de regroupé par section les éléments de notre liste. L’ensemble des fonctionnalités que nous avons vue ci-dessus fonctionnent de la même manière pour la FlatList et la SectionList ce qui va être différent c’est le format des données ainsi qu’une nouvelle propriété nommée renderSectionHeader qui sera utilisée pour afficher notre composant de section.

Format des données

Pour fonctionner la FlatList a besoin comme données d’un tableau que ce soit un tableau d’objet ou bien un tableau de chaine ou autres. Chaque enregistrement du tableau sera alors utilisé comme élément de la liste.

La SectionList quand à elle a aussi besoin d’un tableau seulement celui sera obligatoirement un tableau d’objet. Chaque objet de ce tableau sera considéré comme une section de la liste. A l’intérieur de chaque objet représentant une section nous aurons un attribut qui contiendra un tableau d’objet, chacun de ces objets sera considéré comme un élément de la section nous devrons aussi avoir un attribut qui contiendra les informations dont nous aurons besoin pour notre composant de section.

Dans notre exemple nous allons regroupé les utilisateurs renvoyé par l’api par la première lettre de leur nom. Chaque section affichera la lettre concernée.

Nous allons donc avoir besoin de transformer les données renvoyées par l’api. Pour rendre cette tache plus simple nous allons utiliser lodash qui permet de manipuler facilement des listes et des tableaux.

yarn add lodash

Puisque ce que nous avons vu précédemment est compatible avec la SectionList nous allons dupliquer le fichier UserList.js et le renommer en UserSectionList puis effectuer quelques modifications dedans.

Dans l’import on remplace FlatList par SectionList :

import { SectionList, Text, View, ActivityIndicator, Button, RefreshControl } from 'react-native'

Ensuite nous allons ajouter une fonction _renderSection en dessous de _renderItem :

const _renderSection = ({ section }) => (
  <View style={{ padding: 8, backgroundColor: '#4fc3c8' }}>
    <Text style={{ color: 'white' }}>{section.key.toUpperCase()}</Text>
  </View>
)

Enfin on va remplacer la FlatList par la SectionList, renommer la propriété data par sections et ajouter renderSectionHeader :

<SectionList
  sections={props.data}
  renderSectionHeader={_renderSection}
  renderItem={_renderItem}
  keyExtractor={item => item.email}
  ItemSeparatorComponent={_renderSeparator}
  ListHeaderComponent={_renderHeader}
  ListFooterComponent={() => _renderFooter(props.isFetching, props.hasMoreResult, props.loadMore)}
  ListEmptyComponent={_renderEmpty}
  refreshControl={<RefreshControl refreshing={props.refreshing} onRefresh={props.refresh} />}
/>

Maintenant que nous avons créer notre composant UserSectionList nous avons plus qu’à l’utiliser dans le fichier index.js.

On commence par importer notre nouveau composant UserSectionList et ajouter lodash :

import UserSectionList from './components/UserSectionList'
import _ from 'lodash'

Ensuite nous allons créer une fonction qui va transformer les données renvoyé par l’api aux format attendu par la SectionList :

fromArrayToSectionData(data) {
    let ds = _.groupBy(data, d => d.name.last.charAt(0));
    ds = _.reduce(
      ds,
      (acc, next, index) => {
        acc.push({
          key: index,
          data: next
        });
        return acc;
      },
      []
    );
    ds = _.orderBy(ds, ["key"]);
    return ds;
 }

Ici :

  1. On groupe tous les objets par la première lettre de chaque lastName
  2. On utilise la fonction reduce qui nous permet d’avoir par itération un array d’objet contenant un attributs key qui est la première lettre de lastName et un attribut data qui contient un tableaux d’objet contenant que des éléments dont la valeur du premier caractère de lastName est égale à la valeur de key.
  3. On tri les résultat par la valeur de key.

Pour plus d’information vous pouvez consulter la documentation de lodash.

Maintenant que nous avons la méthode qui formate les données nous allons les rajoutées au state :

state = {
  page: 1,
  results: 20,
  totalPage: 3,
  seed: 'demo',
  isFetching: false,
  data: [],
  hasMoreResult: true,
  refreshing: false,
  formatedData: [],
}

Puis nous allons modifier les méthodes loadData et refreshData pour mettre à jour le state avec les données formatées :

async loadData(page) {
    this.setState({ isFetching: true });
    const data = await this.fetchData(page);
    const nextPage = page + 1;
    const formatedData = this.fromArrayToSectionData(data);
    this.setState({
      page: nextPage,
      data: [...this.state.data, ...data],
      isFetching: false,
      hasMoreResult: nextPage <= this.state.totalPage,
      formatedData: formatedData
    });
  }

  async refreshData() {
    this.setState({ refreshing: true });
    const data = await this.fetchData(1);
    const formatedData = this.fromArrayToSectionData(data);
    this.setState({
      page: 2,
      data: data,
      refreshing: false,
      hasMoreResult: true,
      formatedData: formatedData
    });
  }

Enfin nous remplaçons le composants UserList dans la méthode render par UserSectionList :

render(){
  return(
    <View style={styles.container}>
      <UserSectionList
        data={this.state.formatedData}
        isFetching={this.state.isFetching}
        loadMore={() => this.loadData(this.state.page)}
        hasMoreResult={this.state.hasMoreResult}
        refreshing={this.state.refreshing}
        refresh={() => this.refreshData()}
      />
    </View>
  );
}

Vous devriez obtenir ce résultat :

react native section list

Nous en avons terminer avec l’utilisation des composant FlatList et SectionList.

Vous pouvez retrouver le code source sur github.