Gérer ses chargements en React avec un HOC

Souvent dans une application on à besoin de récuperer des données depuis une API distante. Comme ces données servent ensuite au rendu de l'application, on est obligé d'attendre leur changement avant d'afficher quelque chose. La gestion de ces LoadingState (état de chargement) est important pour l'experience utilisateur et souvent on se retrouve a duplique ce code dans plusieurs parties de l'application. Pourquoi ne pas faire un composant React, qui gerera a la fois ce chargement de façon automatique, mais aussi l'affichage d'un LoadingState.

Les HOC

Un HOC (Higher Order Component) en react est un concept de composant (ou une fonction) dont le seul but est de surcharger un composant React en lui rajoutant des fonctionnalités supplementaires. Souvent, ce composant englobe le composant final (d'ou le Higher Order). Un exemple de HOC connu est le connect de redux qui permet de mapper actions et state au props du composant contenu.

Gestion du chargement

Globalement un composant de chargement a besoin de quelques informations pour fonctionner:

  • isLoaded pour savoir si on a deja chargé quelque chose
  • isLoading pour savoir si on estt en train de charger quelque chose
  • renderLoadingState une render fonction qui determine le rendu lors du chargement
  • renderLoadedState une render fonction qui determine le rendu lorsque l'on a les données
  • fetchData la fonction qui determine le chargement des données

L'idée globale consiste, lorsque ce composant est rendu, a lancer l'action fetchData si uniquement si isLoaded et isLoading sont a false. Tant que isLoading est a true, on affiche le loading state. Enfin, lorsque isLoaded est a true, on affiche renderLoadedState

A partir de cette specification, on peux commencer à écrire le code de notre HOC:

import React from 'react';

const WithLoading = ({ isLoaded, isLoading, renderLoadingState, renderLoadedState, fetchData}) => {
    // Loading hasnt started, but we have no data, let's start it and render LoadingState
    if (!isLoaded && !isLoading) {
        fetchData()
        return renderLoadingState()
    }
    
    // Loading has started, but we still have no data, let's render LoadingState
    if (isLoading) {
        return renderLoadingState()
    }
    
    // Normal case, data has finished loading, let's render LoadedState
    return renderLoadedState()
  }
}

export default WithLoading;

Usage

Avec une telle implementation via les props, notre composant gére le LoadingState, mais reste découplé vis a vis de mecanisme de chargement et de stockage de ces données. On peut ainsi aussi bien utiliser avec des action redux, mais aussi avec un simple stockage dans le state du parent.

Par exemple:

const MyDataContainer = ({ name }) => (<span>Hello {name}</span>)
const MyDataLoader = () => (<span>Loading name, please wait...</span>)

class App extends React.Component {
  state = {
    loading: false,
    data: null
  }
  
  fetchData = () => {
      this.setState({ loading: true })
      fetch(`https://api.github.com/users/atrakeur`)
          .then(json => json.json())
          .then(data => {
             this.setState({ loading: false, data });
          });
  }
  
  isLoading = () => this.state.loading
  
  isLoaded = () => getData() !== null
  
  getData = () => this.state.data

  render() {
    return (<WithLoading 
        renderLoadingState={() => <MyDataLoader />}
        renderLoadedState={() => <MyDataContainer name={this.getData().login} />}
        isLoaded={this.isLoaded()}
        isLoading={this.isLoading()}
        fetchData={this.fetchData}
    />)
  }
}
export default App

Conclusion

Je commence à implementer ce code sur mes prochaines applis. A mon avis il devrait etre suffisamment robuste pour gerer 90% des cas, tout en permettant, dans le pire des cas, de surcharger encore une fois pour gerer les 10% restants.