Redux Toolkit comme outil pour un développement efficace de Redux

image
Actuellement, la part du lion des applications Web basées sur le framework React est développée à l'aide de la bibliothèque Redux. Cette bibliothèque est l'implémentation la plus populaire de l'architecture FLUX et, malgré un certain nombre d'avantages évidents, elle présente des inconvénients très importants, tels que:


  • la complexité et la «verbosité» des modèles recommandés pour l'écriture et l'organisation du code, ce qui implique un grand nombre de passe-partout;
  • le manque de contrôles intégrés pour le comportement asynchrone et les effets secondaires, ce qui conduit à la nécessité de choisir le bon outil parmi une variété de modules complémentaires écrits par des développeurs tiers.

Pour remédier à ces lacunes, les développeurs de Redux ont introduit la bibliothèque Redux Toolkit. Cet outil est un ensemble de solutions et de méthodes pratiques conçues pour simplifier le développement d'applications à l'aide de Redux. Les développeurs de cette bibliothèque avaient pour objectif de simplifier les cas typiques d'utilisation de Redux. Cet outil n'est pas une solution universelle dans chacun des cas possibles d'utilisation de Redux, mais il vous permet de simplifier le code que le développeur doit écrire.


Dans cet article, nous parlerons des principaux outils inclus dans la boîte à outils Redux et, en utilisant un exemple d'un fragment de notre application interne, montrerons comment les utiliser dans le code existant.


En bref sur la bibliothèque


Résumé de la boîte à outils Redux:


  • avant sa sortie, la bibliothèque s'appelait redux-starter-kit;
  • la libération a eu lieu fin octobre 2019;
  • La bibliothèque est officiellement prise en charge par les développeurs Redux.

Selon les développeurs , le Redux Toolkit remplit les fonctions suivantes:


  • Vous aide à démarrer rapidement en utilisant Redux.
  • simplifie le travail avec les tâches typiques et le code Redux;
  • vous permet d'utiliser les meilleures pratiques de Redux par défaut;
  • propose des solutions qui réduisent la méfiance envers les plaques de chaudière.

La boîte à outils Redux fournit un ensemble d'outils à la fois spécialement conçus et ajoute un certain nombre d' outils éprouvés qui sont couramment utilisés avec Redux. Cette approche permet au développeur de décider comment et quels outils utiliser dans son application. Au cours de cet article, nous noterons quels emprunts cette bibliothèque utilise. Pour plus d'informations et les dépendances de la boîte à outils Redux, consultez la description du package @ reduxjs / toolkit .


Les fonctionnalités les plus importantes fournies par la bibliothèque Redux Toolkit sont:


  • #configureStore - une fonction conçue pour simplifier le processus de création et de configuration du stockage;
  • #createReducer - une fonction qui aide à décrire de manière concise et claire et à créer un réducteur;
  • #createAction - renvoie la fonction du créateur de l'action pour la chaîne spécifiée du type d'action;
  • #createSlice - combine les fonctionnalités de createAction et createReducer;
  • createSelector est une fonction de la bibliothèque Reselect , réexportée pour une facilité d'utilisation.

Il convient également de noter que la boîte à outils Redux est entièrement intégrée à TypeScript. Pour plus d'informations, consultez la section Utilisation avec TypeScript de la documentation officielle.


Candidature


Envisagez d'utiliser la bibliothèque Redux Toolkit comme exemple de fragment d'une application React Redux vraiment utilisée.
Remarque Plus loin dans l'article, le code source sera présenté à la fois sans utiliser le Redux Toolkit et avec lui, ce qui permettra de mieux évaluer les aspects positifs et négatifs de l'utilisation de cette bibliothèque.


Défi


Dans l'une de nos applications internes, il était nécessaire d'ajouter, de modifier et d'afficher des informations sur les versions de nos produits logiciels. Pour chacune de ces actions, des fonctions API distinctes ont été développées, dont les résultats doivent être ajoutés au magasin Redux. Pour contrôler le comportement asynchrone et les effets secondaires, nous utiliserons Thunk .


Création de stockage


La version initiale du code source qui crée le référentiel ressemblait à ceci:


import { createStore, applyMiddleware, combineReducers, compose, } from 'redux'; import thunk from 'redux-thunk'; import * as reducers from './reducers'; const ext = window.__REDUX_DEVTOOLS_EXTENSION__; const devtoolMiddleware = ext && process.env.NODE_ENV === 'development' ? ext() : f => f; const store = createStore( combineReducers({ ...reducers, }), compose( applyMiddleware(thunk), devtoolMiddleware ) ); 

Si vous regardez attentivement le code ci-dessus, vous pouvez voir une séquence assez longue d'actions qui doivent être effectuées pour que le stockage soit entièrement configuré. Le Redux Toolkit contient un outil conçu pour simplifier cette procédure, à savoir la fonction configureStore.


Fonction ConfigureStore


Cet outil vous permet de combiner automatiquement les réducteurs, d'ajouter le middleware Redux (la valeur par défaut inclut redux-thunk) et d'utiliser également l'extension Redux DevTools. La fonction configureStore accepte un objet avec les propriétés suivantes comme paramètres d'entrée:


  • réducteur - un ensemble de réducteurs personnalisés,
  • middleware - paramètre facultatif qui spécifie un tableau de middleware conçu pour se connecter au référentiel,
  • devTools - un paramètre de type logique qui vous permet d'activer l'extension Redux DevTools installée dans le navigateur (la valeur par défaut est true),
  • preloadedState - un paramètre facultatif qui définit l'état initial du référentiel,
  • Enhancers - un paramètre facultatif qui définit un ensemble d'amplificateurs.

Pour obtenir la liste la plus populaire de middleware, vous pouvez utiliser la fonction spéciale getDefaultMiddleware, qui fait également partie de la boîte à outils Redux. Cette fonction renvoie un tableau avec le middleware activé par défaut dans la bibliothèque Redux Toolkit. La liste de ces middlewares diffère selon le mode dans lequel votre code est exécuté. En mode production, un tableau se compose d'un seul élément - thunk. En mode développement, au moment de la rédaction, la liste est reconstituée avec le middleware suivant:


  • serializableStateInvariant - un outil spécialement développé pour être utilisé dans le Redux Toolkit et conçu pour vérifier l'arborescence d'état pour la présence de valeurs non sérialisables, telles que les fonctions, Promise, Symbol et d'autres valeurs qui ne sont pas de simples données JS;
  • immutableStateInvariant - middleware du package redux-immutable-state-invariant , conçu pour détecter les mutations dans les données contenues dans le stockage.

Pour spécifier une liste ascendante de middleware, la fonction getDefaultMidlleware accepte un objet qui définit la liste des middlewares inclus et les paramètres pour chacun d'eux. Vous trouverez plus d'informations sur ces informations dans la section correspondante de la documentation officielle.


Nous allons maintenant réécrire la section de code responsable de la création du référentiel à l'aide des outils décrits ci-dessus. En conséquence, nous obtenons ce qui suit:


 import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import * as reducers from './reducers'; const middleware = getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, thunk: true, }); export const store = configureStore({ reducer: { ...reducers }, middleware, devTools: process.env.NODE_ENV !== 'production', }); 

En utilisant l'exemple de cette section de code, vous pouvez clairement voir que la fonction configureStore résout les problèmes suivants:


  • la nécessité de combiner les réducteurs, en appelant automatiquement combineReducers,
  • la nécessité de combiner le middleware, en invoquant automatiquement applyMiddleware.

Il vous permet également d'activer plus facilement l'extension Redux DevTools à l'aide de la fonction composeWithDevTools du package redux-devtools-extension . Tout ce qui précède indique que l'utilisation de cette fonction vous permet de rendre le code plus compact et compréhensible.


Ceci termine la création et la configuration du référentiel. Nous le transférons au fournisseur et continuons.


Actions, créateurs d'actions et réducteur


Examinons maintenant les fonctionnalités de la boîte à outils Redux en termes de développement d'actions, de créateurs d'actions et de réducteurs. La version initiale du code sans utiliser le Redux Toolkit était organisée en fichiers actions.js et reducers.js. Le contenu du fichier actions.js ressemblait à ceci:


 import * as productReleasesService from '../../services/productReleases'; export const PRODUCT_RELEASES_FETCHING = 'PRODUCT_RELEASES_FETCHING'; export const PRODUCT_RELEASES_FETCHED = 'PRODUCT_RELEASES_FETCHED'; export const PRODUCT_RELEASES_FETCHING_ERROR = 'PRODUCT_RELEASES_FETCHING_ERROR'; … export const PRODUCT_RELEASE_UPDATING = 'PRODUCT_RELEASE_UPDATING'; export const PRODUCT_RELEASE_UPDATED = 'PRODUCT_RELEASE_UPDATED'; export const PRODUCT_RELEASE_CREATING_UPDATING_ERROR = 'PRODUCT_RELEASE_CREATING_UPDATING_ERROR'; function productReleasesFetching() { return { type: PRODUCT_RELEASES_FETCHING }; } function productReleasesFetched(productReleases) { return { type: PRODUCT_RELEASES_FETCHED, productReleases }; } function productReleasesFetchingError(error) { return { type: PRODUCT_RELEASES_FETCHING_ERROR, error } } … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched(productReleases)) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError(error)) }); } } … export function updateProductRelease( id, productName, productVersion, releaseDate ) { return dispatch => { dispatch(productReleaseUpdating()); return productReleasesService.updateProductRelease( id, productName, productVersion, releaseDate ).then( productRelease => dispatch(productReleaseUpdated(productRelease)) ).catch(error => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseCreatingUpdatingError(error)) }); } } 

Contenu du fichier reducers.js avant d'utiliser Redux Toolkit:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', updatingState: 'none', error: null, }; export default function reducer(state = initialState, action = {}) { switch (action.type) { case productReleases.PRODUCT_RELEASES_FETCHING: return { ...state, fetchingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASES_FETCHED: return { ...state, productReleases: action.productReleases, fetchingState: 'success', }; case productReleases.PRODUCT_RELEASES_FETCHING_ERROR: return { ...state, fetchingState: 'failed', error: action.error }; … case productReleases.PRODUCT_RELEASE_UPDATING: return { ...state, updatingState: 'requesting', error: null, }; case productReleases.PRODUCT_RELEASE_UPDATED: return { ...state, updatingState: 'success', productReleases: state.productReleases.map(productRelease => { if (productRelease.id === action.productRelease.id) return action.productRelease; return productRelease; }) }; case productReleases.PRODUCT_RELEASE_UPDATING_ERROR: return { ...state, updatingState: 'failed', error: action.error }; default: return state; } } 

Comme nous pouvons le voir, c'est là que la majeure partie du passe-partout est contenue: constantes de type d'action, créateurs d'action, constantes à nouveau, mais dans le code réducteur, il faut du temps pour écrire tout ce code. Vous pouvez vous débarrasser partiellement de ce passe-partout en utilisant les fonctions createAction et createReducer, qui font également partie de la boîte à outils Redux.


Fonction CreateAction


Dans la section de code donnée, la méthode standard pour définir une action dans Redux est utilisée: d'abord, une constante est définie séparément qui détermine le type d'action, puis - la fonction du créateur de l'action de ce type. La fonction createAction combine ces deux déclarations en une seule. En entrée, il prend un type d'action et renvoie le créateur de l'action pour ce type. Le créateur de l'action peut être appelé soit sans arguments, soit avec un argument (charge utile), dont la valeur sera placée dans le champ de charge utile de l'action créée. En outre, le créateur d'action remplace la fonction toString (), de sorte que le type d'action devient sa représentation sous forme de chaîne.


Dans certains cas, vous devrez peut-être écrire une logique supplémentaire pour ajuster la valeur de la charge utile, par exemple, accepter plusieurs paramètres pour le créateur de l'action, créer un identificateur aléatoire ou obtenir l'horodatage actuel. Pour ce faire, createAction prend un deuxième argument facultatif - une fonction qui sera utilisée pour mettre à jour la valeur de la charge utile. Vous trouverez plus d'informations sur ce paramètre dans la documentation officielle.
En utilisant la fonction createAction, nous obtenons le code suivant:


 export const productReleasesFetching = createAction('PRODUCT_RELEASES_FETCHING'); export const productReleasesFetched = createAction('PRODUCT_RELEASES_FETCHED'); export const productReleasesFetchingError = createAction('PRODUCT_RELEASES_FETCHING_ERROR'); … export function fetchProductReleases() { return dispatch => { dispatch(productReleasesFetching()); return productReleasesService.getProductReleases().then( productReleases => dispatch(productReleasesFetched({ productReleases })) ).catch(error => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })) }); } } ... 

Fonction CreateReducer


Considérez maintenant le réducteur. Comme dans notre exemple, les réducteurs sont souvent implémentés à l'aide de l'instruction switch, avec un registre pour chaque type d'action traité. Cette approche fonctionne bien, mais n'est pas sans passe-partout et sujette aux erreurs. Par exemple, il est facile d'oublier de décrire le cas par défaut ou de ne pas définir l'état initial. La fonction createReducer simplifie la création de fonctions de réduction en les définissant comme des tables de recherche de fonctions pour traiter chaque type d'action. Il vous permet également de simplifier considérablement la logique des mises à jour immuables en écrivant du code dans un style "mutable" à l'intérieur des réducteurs.


Un style de gestion d'événements robuste est disponible grâce à l'utilisation de la bibliothèque Immer . La fonction de gestionnaire peut soit «muter» l'état passé pour changer les propriétés, soit renvoyer un nouvel état, comme lorsque l'on travaille dans le style immuable, mais grâce à Immer, la vraie mutation de l'objet n'est pas effectuée. La première option est beaucoup plus facile pour le travail et la perception, en particulier lors du changement d'un objet avec une imbrication profonde.


Attention: le retour d'un nouvel objet à partir d'une fonction annule les modifications «mutables». L'utilisation simultanée des deux méthodes de mise à jour d'état ne fonctionnera pas.


La fonction createReducer accepte les arguments suivants comme paramètres d'entrée:


  • état initial de stockage
  • un objet qui établit une correspondance entre les types d'actions et les réducteurs, dont chacun traite un certain type.

En utilisant la méthode createReducer, nous obtenons le code suivant:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const counterReducer = createReducer(initialState, { [productReleasesFetching]: (state, action) => { state.fetchingState = 'requesting' }, [productReleasesFetched.type]: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, [productReleasesFetchingError]: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … [productReleaseUpdating]: (state) => { state.updatingState = 'requesting' }, [productReleaseUpdated]: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, [productReleaseUpdatingError]: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }); 

Comme nous pouvons le voir, l'utilisation des fonctions createAction et createReducer résout essentiellement le problème de l'écriture de code supplémentaire, mais le problème de la création de constantes au préalable demeure. Par conséquent, nous considérons une option plus puissante qui combine la génération de créateurs d'action et de réducteur - la fonction createSlice.


Fonction CreateSlice


La fonction createSlice accepte un objet avec les champs suivants comme paramètres d'entrée:


  • name - espace de noms des actions créées ( ${name}/${action.type} );
  • initialState - état initial du réducteur;
  • réducteurs - un objet avec des gestionnaires. Chaque gestionnaire prend une fonction avec l'état et l'action des arguments, l'action contient des données dans la propriété payload et le nom de l'événement dans la propriété name. De plus, il est possible de modifier au préalable les données reçues de l'événement avant qu'il n'entre dans le réducteur (par exemple, ajouter id aux éléments de la collection). Pour ce faire, au lieu d'une fonction, vous devez passer un objet avec le réducteur et préparer les champs, où réducteur est la fonction de gestionnaire d'action et préparer est la fonction de gestionnaire de charge utile qui renvoie la charge utile mise à jour;
  • extraReducers - un objet contenant des réducteurs d'une autre tranche. Ce paramètre peut être requis s'il est nécessaire de mettre à jour un objet appartenant à une autre tranche. Vous pouvez en savoir plus sur cette fonctionnalité dans la section correspondante de la documentation officielle.

Le résultat de la fonction est un objet appelé "tranche", avec les champs suivants:


  • nom - nom de tranche,
  • réducteur - réducteur,
  • actions - un ensemble d'actions.

En utilisant cette fonction pour résoudre notre problème, nous obtenons le code source suivant:


 const initialState = { productReleases: [], loadedProductRelease: null, fetchingState: 'none', creatingState: 'none', loadingState: 'none', error: null, }; const productReleases = createSlice({ name: 'productReleases', initialState, reducers: { productReleasesFetching: (state) => { state.fetchingState = 'requesting'; }, productReleasesFetched: (state, action) => { state.productReleases = action.payload.productReleases; state.fetchingState = 'success'; }, productReleasesFetchingError: (state, action) => { state.fetchingState = 'failed'; state.error = action.payload.error; }, … productReleaseUpdating: (state) => { state.updatingState = 'requesting' }, productReleaseUpdated: (state, action) => { state.updatingState = 'success'; state.productReleases = state.productReleases.map(productRelease => { if (productRelease.id === action.payload.productRelease.id) return action.payload.productRelease; return productRelease; }); }, productReleaseUpdatingError: (state, action) => { state.updating = 'failed'; state.error = action.payload.error; }, }, }); 

Nous allons maintenant extraire les créateurs d'action et le réducteur de la tranche créée.


 const { actions, reducer } = productReleases; export const { productReleasesFetched, productReleasesFetching, productReleasesFetchingError, … productReleaseUpdated, productReleaseUpdating, productReleaseUpdatingError } = actions; export default reducer; 

Le code source des créateurs d'actions contenant les appels d'API n'a pas changé, à l'exception de la méthode de transmission des paramètres lors de l'envoi des actions:


 export const fetchProductReleases = () => (dispatch) => { dispatch(productReleasesFetching()); return productReleasesService .getProductReleases() .then((productReleases) => dispatch(productReleasesFetched({ productReleases }))) .catch((error) => { error.clientMessage = "Can't get product releases"; dispatch(productReleasesFetchingError({ error })); }); }; … export const updateProductRelease = (id, productName, productVersion, releaseDate) => (dispatch) => { dispatch(productReleaseUpdating()); return productReleasesService .updateProductRelease(id, productName, productVersion, releaseDate) .then((productRelease) => dispatch(productReleaseUpdated({ productRelease }))) .catch((error) => { error.clientMessage = "Can't update product releases"; dispatch(productReleaseUpdatingError({ error })); }); 

Le code ci-dessus montre que la fonction createSlice vous permet de vous débarrasser d'une partie importante du passe-partout lorsque vous travaillez avec Redux, ce qui vous permet non seulement de rendre le code plus compact, concis et compréhensible, mais aussi de passer moins de temps à l'écrire.


Résumé


À la fin de cet article, je voudrais dire que malgré le fait que la bibliothèque Redux Toolkit n'ajoute rien de nouveau à la gestion du stockage, elle fournit un certain nombre de moyens beaucoup plus pratiques pour écrire du code qu'auparavant. Ces outils permettent non seulement de rendre le processus de développement plus pratique, compréhensible et plus rapide, mais aussi plus efficace en raison de la présence dans la bibliothèque d'un certain nombre d'outils éprouvés. Nous, Inobitek, prévoyons de continuer à utiliser cette bibliothèque dans le développement de nos produits logiciels et de suivre les nouveaux développements prometteurs dans le domaine des technologies Web.


Merci de votre attention. Nous espérons que notre article vous sera utile. Plus d'informations sur la bibliothèque Redux Toolkit peuvent être obtenues dans la documentation officielle.

Source: https://habr.com/ru/post/fr481288/


All Articles