Centraliser les messages d’erreurs sous React avec Redux
Le but de cet article est de vous proposer une solution permettant de centraliser vos messages d’erreur émis par vos réponses HTTP depuis un composant React intégrant par exemple l’affichage d’une modal.
Dans la version 16 de React, la notion d’Error Boundary a été introduite permettant d’intercepter les messages d’erreur JavaScript au niveau du composant React via la méthode componentDidCatch(error, info)
. Cependant cette détection d’erreur n’intervient que lors du rendu du composant. Comment faire du coup pour intercepter et gérer de la même manière un message provenant d’une réponse de notre BackEnd ?
Cet article part de l’idée que vous avez déjà quelques notions d’utilisation de React et que vous partez d’un projet existant. Cependant si vous débutez je vous invite à lire l’article suivant qui vous indique comment déployer un projet React : Initialiser un projet React sous TypeScript avec Webpack. Et pour vous familiariser avec React rien de mieux que le tutoriel officiel: https://reactjs.org/tutorial/tutorial.html
Interception des réponses à l’aide du client HTTP
Dans un développement classique nous utilisons l’instruction try ... catch
pour intercepter les exceptions fournis par la réponse HTTP de notre promesse, mais cela fonctionne un peu moins bien si on décide de rationaliser le code de manière à centraliser les retours d’erreur vers un seul composant. Heureusement pour nous la plupart des clients HTTP proposent le mécanisme d’interception qui permet d’agir lors de requêtes / réponses HTTP. Dans le cas de React qui n’inclus pas de client HTTP par défaut, nous utiliserons le client HTTP axios.
Importez le dans le fichier de point d’entrée de votre application, généralement index.js
:
import axios from 'axios';
Puis ajoutez le code suivant :
// Response interceptor.
axios.interceptors.response.use(
function(response) {
// Do something with response data
return response;
},
function(error) {
// Do something with response error
return Promise.reject(error);
}
);
Grâce à ça, vous aller pouvoir interagir avec les messages d’erreur retournés par votre BackEnd.
Redux à la rescousse
Ce que nous voulons c’est qu’un composant React réagisse quand un message d’erreur est levé par le BackEnd. Pour nous y aider nous allons utiliser la librairie Redux. Redux permet de gérer la persistance de données. Elle vous permet de maintenir un store contenant des données et pouvant être mis à jour via des actions définies. Lorsque ce store évolue, une mécanique permet de mettre à jour l’UI de votre application avec les nouvelles données.
Installer Redux
Pour mettre en place Redux dans votre projet, suivez simplement le rapide tutoriel fournis par Redux : https://redux.js.org/basics/usage-with-react
Notre besoin corresponds donc à :
- Intercepter le message d’erreur via la mécanique d’interception de notre client HTTP axios.
- Stocker le retour d’erreur dans le store Redux.
- Dispatcher l’action correspondant à notre erreur dans l’UI.
- Mettre à jour notre UI avec le message d’erreur.
Définir les actions Redux
Dans votre projet ajoutez un dossier Redux
, qui contiendra un dossier Actions
et Reducers
. Ces deux derniers dossiers vont contenir les actions qui dispatcherons les nouvelles données dans votre store et les modifications à apporter au state de votre application. Cette dernière aura pour incidence de lancer un nouveau rendu de votre UI.
Pensez à ajouter une constante dans votre projet qui définit le type d’actions, en l’occurrence une action correspondant à la gestion des retours d’erreur, mais aussi une action qui nous permettrait de remettre à zéro notre message d’erreur. Ces valeurs seront utilisées à tous les niveaux de votre application. Vous pouvez simplement créer un fichier à la racine du dossier Redux > actionTypes.js
contenant les lignes suivantes :
export const RESPONSE_ERROR = 'RESPONSE_ERROR';
export const CLEAR_ERROR = 'CLEAR_ERROR';
Vous devez ensuite définir les actions dans le fichier actions > error.js
:
import * as types from '../actionTypes';
export const loadErrors = error => ({
type: types.RESPONSE_ERROR,
error
});
export const clearError = () => ({
type: types.CLEAR_ERROR
});
La première constante définit une méthode retournant l’action correspondant à notre retour d’erreur ainsi que l’objet contenant l’erreur elle même. Le type de l’objet erreur est à votre convenance mais pour des raisons pratiques pour l’élaboration de notre article, disons qu’il sera de cette forme : { type: string, message: string, date: date }
La deuxième définit simplement une action reset de notre message. Pas besoin ici de fournir de données supplémentaires.
Définir les réducteurs (Reducers) Redux
Le réducteur permet à Redux de modifier l’état du store qui sera émis vers l’application en fonction d’une action.
import * as types from '../actionTypes';
const initialState = {};
export default (state = initialState, action) => {
if (action.type === types.RESPONSE_ERROR) {
return { ...state, ...action.error };
}
if (action.type === types.CLEAR_ERROR) {
return initialState;
}
return state;
};
Ici, nous apercevons nos deux actions potentiellement émises ainsi que les routines effectuées sur le store. Dans le cas d’une action de type RESPONSE_ERROR
nous mettons à jour le state de l’application avec le nouvel objet error reçu. Dans le cas d’un CLEAR_ERROR
le state est simplement réinitialisé.
Dispatche de l’erreur via l’interception
Le dispatche de l’erreur s’effectue depuis l’interception que nous avons inséré dans notre index.js
. Modifiez votre code de la manière suivante :
import { loadErrors } from './redux/actions/error';
// Response interceptor.
axios.interceptors.response.use(
function(response) {
// Do something with response data
return response;
},
function(error) {
store.dispatch(loadErrors(error));
return Promise.reject(error);
}
);
store corresponds à une instance de votre store Redux. Pour que votre store fonctionne correctement vous avez besoin d’injecter une instance de la méthode combineReducers()
. Son implémentation peut se résumer de la manière suivante :
- Ajoutez dans le dossier
Reducers
un fichierindex.js
. - Copiez le code suivant dans le fichier :
import { combineReducers } from 'redux';
import error from './error';
export default combineReducers({
error
});
L’écriture de l’instanciation de la méthode combineReducers()
utilise la notation abrégée des propriétés sous ES6 (vous l’avez déjà rencontré lors de l’écriture des actions). Cela équivaux à écrire ceci :
export default combineReducers({
error: error
});
Cette méthode permet de combiner plusieurs réducteurs en une seule déclaration pour votre store. Si nous devions maintenant ajouter rapidement une instance de notre store dans index.js
, cela pourrait ressembler à ça :
import { createStore } from 'redux';
import rootReducer from './reducers/index';
const store = createStore(rootReducer);
// Response interceptor.
axios.interceptors.response.use(
function(response) {
// Do something with response data
return response;
},
function(error) {
store.dispatch(loadErrors(error));
return Promise.reject(error);
}
);
N’oubliez pas cependant que tout ceci n’est qu’une partie de la configuration de Redux dans votre application React, vous devez impérativement lire les tutoriaux pour une intégration et compréhension complète de cet outil.
Affichage de l’erreur dans l’UI
Maintenant que notre erreur est interceptée et dispatchée dans le store, nous allons pouvoir l’afficher dans notre application. Nous avons besoin pour cela d’un composant React connecté au store :
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { clearError } from '../redux/actions/error';
class ErrorHandler extends React.Component{
constructor(props){
super(props);
this.state = {
showError: false
};
}
componentDidUpdate(prevProps) {
// Check error message.
if(this.props.error.hasOwnProperty('message') &&
(!prevProps.error.hasOwnProperty('message') || this.props.error.date !== prevProps.error.date)) {
this.setState({showError: true});
}
}
handleClick = () => {
this.props.clearError();
this.setState({showError: false});
}
render() {
return this.state.showError && <div >
<h2>Error: {this.props.error.type}</h2>
<p>{this.props.error.message}</p>
<button onClick={this.handleClick} />
</div>
}
}
ErrorHandler.propTypes = {
error: PropTypes.object,
clearError: PropTypes.func
}
const mapStateToProps = state => ({
error: state.error
});
const mapDispatchToProps = dispatch => {
return {
clearError: () => { dispatch(clearError()); }
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ErrorHandler);
Plusieurs notions sont abordées ici. Premièrement la méthode connect()
lors de l’export de notre composant qui permet d’injecter vers Redux les mappings à effectuer entre le state React et le store Redux (voir Connect: Extracting Data with mapStateToProps). Mais également de définir les liaisons d’appels vers le store des méthodes de dispatche des actions (voir Connect: Dispatching Actions with mapDispatchToProps).
Nous utilisons également les méthodes de cycle de vie du composant pour valider l’état de notre objet error, ici en l’occurrence componentDidUpdate()
. Quand nous interceptons le message d’erreur, celui-ci est dispatché vers le store Redux. Une action est levée et met à jour l’état de notre store. Ceci a pour effet de modifier également l’état de notre composant grâce à la connexion établie via la méthode mapStateToProps()
. Un rendu est alors effectué par React provoquant un nouveau cycle de vie et surtout une évaluation du contenu de notre objet error.
Le tour est joué, il ne vous reste plus qu’à importer le composant ErrorHandler
dans votre application et d’ajouter une déclaration :
import ErrorHandler from './components/errorHandler';
const render = Component => {
ReactDOM.render(
<Provider store={store}>
// Your application navigation
<ErrorHandler />
</Provider>
document.getElementById('root')
);
};
// Render once
render(App);
Ressources
- Tutoriel React: https://reactjs.org/tutorial/tutorial.html
- Error Handling in React 16: https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html
- GitHub Axios: https://github.com/axios/axios
- React Redux: https://react-redux.js.org/
- Définir un store Redux: https://redux.js.org/basics/store
- mapStateToProps: https://react-redux.js.org/using-react-redux/connect-mapstate
- mapDispatchToProps: https://react-redux.js.org/using-react-redux/connect-mapdispatch