Dans un écosystème toujours plus grand d’applications JavaScript riches et complexes, il y a plus d’états à gérer que jamais: l’utilisateur actuel, la liste des publications chargées, etc.
Tout ensemble de données nécessitant un historique des événements peut être considéré comme avec état. La gestion de l'état peut être difficile et sujette aux erreurs, mais travailler avec des données immuables (plutôt que mutables) et certaines technologies de support, à savoir Redux, aux fins de cet article, peut aider de manière significative.
Les données immuables ont des restrictions, à savoir qu'elles ne peuvent pas être modifiées une fois qu'elles ont été créées, mais elles présentent également de nombreux avantages, en particulier en ce qui concerne l'égalité entre les références et les valeurs, ce qui peut considérablement accélérer les applications qui reposent sur des comparaisons fréquentes de données (vérifier si quelque chose doit être mis à jour , par exemple).
L'utilisation d'états immuables nous permet d'écrire du code qui peut rapidement dire si l'état a changé, sans avoir besoin de faire une comparaison récursive sur les données, ce qui est généralement beaucoup, beaucoup plus rapide.
Cet article couvrira les applications pratiques de Redux lors de la gestion de l'état via des créateurs d'action, des fonctions pures, des réducteurs composés, des actions impures avec Redux-saga et Redux Thunk et, enfin, l'utilisation de Redux avec React. Cela dit, il existe de nombreuses alternatives à Redux, telles que les bibliothèques basées sur MobX, Relay et Flux.
L'aspect clé qui sépare Redux de la plupart des autres conteneurs d'état tels que MobX, Relay et la plupart des autres implémentations basées sur Flux est que Redux a un seul état qui ne peut être modifié que via des «actions» (objets JavaScript simples), qui sont distribuées au Magasin Redux. La plupart des autres magasins de données ont l'état contenu dans les composants React eux-mêmes, vous permettent d'avoir plusieurs magasins et / ou d'utiliser l'état mutable.
Cela entraîne à son tour le réducteur du magasin, une fonction pure qui opère sur des données immuables, à exécuter et potentiellement mettre à jour l'état. Ce processus applique un flux de données unidirectionnel, qui est plus facile à comprendre et plus déterministe.
Puisque les réducteurs Redux sont des fonctions pures fonctionnant sur des données immuables, ils produisent toujours la même sortie avec la même entrée, ce qui les rend faciles à tester. Voici un exemple de réducteur:
import Immutable from 'seamless-immutable' const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { return state.concat(action.payload) } return state // note that a reducer MUST return a value } // somewhere else... store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user
La gestion des fonctions pures permet à Redux de prendre facilement en charge de nombreux cas d'utilisation qui ne sont généralement pas faciles à réaliser avec un état mutatif, tels que:
Les créateurs d'action de Redux aident à garder le code propre et testable. N'oubliez pas que les «actions» dans Redux ne sont rien de plus que des objets JavaScript simples décrivant une mutation qui devrait se produire. Cela étant dit, écrire les mêmes objets encore et encore est répétitif et sujet aux erreurs.
apprentissage semi-supervisé apprentissage profond
Un créateur d'action dans Redux est simplement une fonction d'assistance qui renvoie un objet JavaScript simple décrivant une mutation. Cela permet de réduire le code répétitif et de conserver toutes vos actions au même endroit:
export function usersFetched(users) { return { type: 'USERS_FETCHED', payload: users, } } export function usersFetchFailed(err) { return { type: 'USERS_FETCH_FAILED', payload: err, } } // reducer somewhere else... const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function usersFetchedReducer(state = initialState, action) { if (action.type === 'USERS_FETCHED') { return Immutable(action.payload) } return state // note that a reducer MUST return a value }
Bien que la nature même des réducteurs et des actions les rend faciles à tester, sans bibliothèque d'aide à l'immuabilité, rien ne vous protège des objets en mutation, ce qui signifie que les tests de tous vos réducteurs doivent être particulièrement robustes.
Prenons l'exemple de code suivant d'un problème que vous rencontrerez sans bibliothèque pour vous protéger:
const initialState = [] function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { state.push(action.payload) // NOTE: mutating action!! return state } return state // note that a reducer MUST return a value }
Dans cet exemple de code, le voyage dans le temps sera interrompu car l'état précédent sera désormais le même que l'état actuel, les composants purs peuvent potentiellement ne pas être mis à jour (ou restituer) car la référence à l'état n'a pas changé même si les données contient a changé et les mutations sont beaucoup plus difficiles à raisonner.
Sans bibliothèque d'immuabilité, nous perdons tous les avantages fournis par Redux. Il est donc fortement recommandé d’utiliser une bibliothèque d’aide à l’immuabilité, telle que immutable.js ou parfaitement immuable, en particulier lorsque vous travaillez dans une grande équipe avec plusieurs mains touchant le code.
Quelle que soit la bibliothèque que vous utilisez, Redux se comportera de la même manière. Comparons les avantages et les inconvénients des deux afin de pouvoir choisir celui qui convient le mieux à votre cas d'utilisation:
Immutable.js est une bibliothèque, construite par Facebook, avec un style plus fonctionnel sur les structures de données, telles que les cartes, les listes, les ensembles et les séquences. Sa bibliothèque de structures de données persistantes immuables effectue le moins de copies possible entre les différents états.
Avantages:
la loi sur le steagall en verre abroger les conséquences
Les inconvénients:
Seamless-immutable est une bibliothèque de données immuables qui est rétrocompatible jusqu'à ES5.
Il est basé sur des fonctions de définition de propriété ES5, telles que defineProperty(..)
pour désactiver les mutations sur les objets. En tant que tel, il est entièrement compatible avec les bibliothèques existantes telles que lodash et Ramda. Il peut également être désactivé dans les versions de production, offrant un gain de performances potentiellement significatif.
Avantages:
Les inconvénients:
Une autre fonctionnalité utile de Redux est la possibilité de composer des réducteurs ensemble, ce qui permet de créer des applications beaucoup plus compliquées, et dans une application de toute taille appréciable, vous aurez forcément plusieurs types d'états (utilisateur actuel, liste des articles chargés, etc). Redux prend en charge (et encourage) ce cas d'utilisation en fournissant naturellement la fonction combineReducers
:
import { combineReducers } from 'redux' import currentUserReducer from './currentUserReducer' import postsListReducer from './postsListReducer' export default combineReducers({ currentUser: currentUserReducer, postsList: postsListReducer, })
Avec le code ci-dessus, vous pouvez avoir un composant qui repose sur le currentUser
et un autre composant qui repose sur postsList
. Cela améliore également les performances car tout composant unique ne s'abonnera qu'à la ou aux branches de l'arborescence les concernant.
Par défaut, vous ne pouvez envoyer que des objets JavaScript simples à Redux. Cependant, avec le middleware, Redux peut prendre en charge des actions impures telles que l'obtention de l'heure actuelle, l'exécution d'une requête réseau, l'écriture d'un fichier sur le disque, etc.
«Middleware» est le terme utilisé pour les fonctions qui peuvent intercepter les actions en cours de distribution. Une fois intercepté, il peut faire des choses comme transformer l'action ou envoyer une action asynchrone, un peu comme le middleware dans d'autres frameworks (comme Express.js).
Deux bibliothèques middleware très courantes sont Redux Thunk et Redux-saga. Redux Thunk est écrit dans un style impératif, tandis que Redux-saga est écrit dans un style fonctionnel. Comparons les deux.
Redux Thunk prend en charge les actions impures dans Redux en utilisant des thunks, des fonctions qui renvoient d'autres fonctions chaînables. Pour utiliser Redux-Thunk, vous devez d'abord monter le middleware Redux Thunk sur le magasin:
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' const store = createStore( myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R )
Maintenant, nous pouvons effectuer des actions impures (telles que l'exécution d'un appel API) en envoyant un thunk au magasin Redux:
store.dispatch( dispatch => { return api.fetchUsers() .then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action) .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError } )
Il est important de noter que l’utilisation de thunks peut rendre votre code difficile à tester et compliquer le raisonnement dans le flux de code.
Redux-saga soutient les actions impures à travers un ES6 (ES2015) fonctionnalité appelée générateurs et une bibliothèque de helpers fonctionnels / purs. L'avantage des générateurs est qu'ils peuvent être repris et suspendus, et leur contrat API les rend extrêmement faciles à tester.
Voyons comment nous pouvons améliorer la lisibilité et la testabilité de la méthode Thunk précédente en utilisant des sagas!
Tout d'abord, montons le middleware Redux-saga dans notre boutique:
import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './rootReducer' import rootSaga from './rootSaga' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount the middleware to the store const store = createStore( rootReducer, applyMiddleware(sagaMiddleware), ) // run our saga! sagaMiddleware.run(rootSaga)
Notez que le run(..)
La fonction doit être appelée avec la saga pour qu'elle commence à s'exécuter.
Créons maintenant notre saga:
import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use export function *fetchUsers(action) { try { const users = yield call(api.fetchUsers) yield put(usersFetched(users)) } catch (err) { yield put(usersFetchFailed(err)) } } export default function *rootSaga() { yield takeEvery('USERS_FETCH', fetchUsers) }
Nous avons défini deux fonctions génératrices, l'une qui récupère la liste des utilisateurs et le rootSaga
. Notez que nous n’avons pas appelé api.fetchUsers
directement, mais à la place, il l'a donné dans un objet d'appel. C'est parce que Redux-saga intercepte l'objet d'appel et exécute la fonction contenue à l'intérieur pour créer un environnement pur (en ce qui concerne vos générateurs).
rootSaga
renvoie un seul appel à une fonction appelée takeEvery,
qui prend chaque action distribuée avec un type de USERS_FETCH
et appelle le fetchUsers
saga avec l'action qu'elle a prise. Comme nous pouvons le voir, cela crée un modèle d'effets secondaires très prévisible pour Redux, ce qui le rend facile à tester!
Voyons comment les générateurs rendent nos sagas faciles à tester. Nous utiliserons moka dans cette partie pour exécuter nos tests unitaires et chai pour les assertions.
pourquoi devrais-je utiliser node.js
Parce que les sagas produisent des objets JavaScript simples et sont exécutées dans un générateur, nous pouvons facilement tester qu'elles exécutent le bon comportement sans aucune moquerie! Gardez à l'esprit que call
, take
, put
, etc. ne sont que des objets JavaScript simples qui sont interceptés par le middleware Redux-saga.
import { take, call } from 'redux-saga/effects' import { expect } from 'chai' import { rootSaga, fetchUsers } from '../rootSaga' describe('saga unit test', () => { it('should take every USERS_FETCH action', () => { const gen = rootSaga() // create our generator iterable expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely }) it('should fetch the users if successful', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const users = [ user1, user2 ] // some mock response expect(gen.next(users).value).to.be.eql(put(usersFetched(users)) }) it('should fail if API fails', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const err = { message: 'authentication failed' } // some mock error expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err)) }) })
Bien que Redux ne soit lié à aucune bibliothèque compagnon spécifique, il fonctionne particulièrement bien avec React.js puisque les composants React sont des fonctions pures qui prennent un état en entrée et produisent un DOM virtuel en sortie.
React-Redux est une bibliothèque d'aide pour React et Redux qui élimine la plupart du travail acharné reliant les deux. Pour utiliser React-Redux le plus efficacement possible, passons en revue la notion de composants de présentation et de composants de conteneur.
taux de facturation vs calculateur de salaire
Les composants de présentation décrivent à quoi les choses devraient ressembler visuellement, en fonction uniquement de leurs accessoires à rendre; ils invoquent des rappels depuis les accessoires pour envoyer des actions. Ils sont écrits à la main, totalement purs et ne sont pas liés à des systèmes de gestion d’états comme Redux.
Les composants du conteneur, quant à eux, décrivent comment les choses devraient fonctionner, sont conscients de Redux, distribuent des actions Redux directement pour effectuer des mutations et sont généralement générés par React-Redux. Ils sont souvent associés à un composant de présentation, fournissant ses accessoires.
Écrivons un composant de présentation et connectons-le à Redux via React-Redux:
const HelloWorld = ({ count, onButtonClicked }) => ( Hello! You've clicked the button {count} times! Click me ) HelloWorld.propTypes = { count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }
Notez qu'il s'agit d'un composant «stupide» qui repose entièrement sur ses accessoires pour fonctionner. C'est génial, car cela rend le Composant React facile à tester et à composer . Voyons comment connecter ce composant à Redux maintenant, mais voyons d'abord ce qu'est un composant d'ordre supérieur.
React-Redux fournit une fonction d'assistance appelée connect( .. )
qui crée un composant d'ordre supérieur à partir d'un composant React «stupide» qui est conscient de Redux.
React met l'accent sur l'extensibilité et la réutilisation via la composition, c'est-à-dire lorsque vous enveloppez des composants dans d'autres composants. L'emballage de ces composants peut modifier leur comportement ou ajouter de nouvelles fonctionnalités. Voyons comment nous pouvons créer un composant d'ordre supérieur à partir de notre composant de présentation qui est conscient de Redux - un composant de conteneur.
Voici comment procéder:
import { connect } from 'react-redux' const mapStateToProps = state => { // state is the state of our store // return the props that we want to use for our component return { count: state.count, } } const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function // return the props that we want to use for our component return { onButtonClicked: () => { dispatch({ type: 'BUTTON_CLICKED' }) }, } } // create our enhancer function const enhancer = connect(mapStateToProps, mapDispatchToProps) // wrap our 'dumb' component with the enhancer const HelloWorldContainer = enhancer(HelloWorld) // and finally we export it export default HelloWorldContainer
Notez que nous avons défini deux fonctions, mapStateToProps
et mapDispatchToProps
.
mapStateToProps
est une fonction pure de (state: Object) qui renvoie un objet calculé à partir de l'état Redux. Cet objet sera fusionné avec les accessoires passés au composant encapsulé. Ceci est également connu sous le nom de sélecteur, car il sélectionne des parties de l'état Redux à fusionner dans les accessoires du composant.
mapDispatchToProps
est également une fonction pure, mais l'une des (dispatch: (Action) => void) qui renvoie un objet calculé à partir de la fonction de répartition Redux. Cet objet sera également fusionné avec les accessoires passés au composant encapsulé.
Maintenant, pour utiliser notre composant conteneur, nous devons utiliser le Provider
composant dans React-Redux pour indiquer au composant conteneur quel magasin utiliser:
import { Provider } from 'react-redux' import { render } from 'react-dom' import store from './store' // where ever your Redux store resides import HelloWorld from './HelloWorld' render( ( ), document.getElementById('container') )
Le Provider
Le composant propage le magasin à tous les composants enfants qui s'abonnent au magasin Redux, gardant tout au même endroit et réduisant les points d'erreur ou de mutation!
Avec cette nouvelle connaissance de Redux, ses nombreuses bibliothèques de support et sa connexion au framework avec React.js, vous pouvez facilement limiter le nombre de mutations dans votre application grâce au contrôle d'état. Un contrôle d'état solide, à son tour, vous permet de vous déplacer plus rapidement et de créer une base de code solide avec plus de confiance.