Création d'applications réactives avec Redux, RxJS et Redux-Observable dans React Native

Publié: 2022-03-11

Dans l'écosystème croissant d'applications Web et mobiles riches et puissantes, il y a de plus en plus d'états à gérer, comme l'utilisateur actuel, la liste des éléments chargés, l'état de chargement, les erreurs et bien plus encore. Redux est une solution à ce problème en gardant l'état dans un objet global.

L'une des limites de Redux est qu'il ne prend pas en charge le comportement asynchrone prêt à l'emploi. Une solution pour cela est redux-observable , qui est basée sur RxJS, une puissante bibliothèque pour la programmation réactive en JavaScript. RxJS est une implémentation de ReactiveX, une API de programmation réactive originaire de Microsoft. ReactiveX combine certaines des fonctionnalités les plus puissantes du paradigme réactif, de la programmation fonctionnelle, du modèle d'observateur et du modèle d'itérateur.

Dans ce didacticiel, nous découvrirons Redux et son utilisation avec React. Nous explorerons également la programmation réactive à l'aide de RxJS et comment elle peut rendre très simple le travail asynchrone fastidieux et complexe.

Enfin, nous apprendrons redux-observable, une bibliothèque qui exploite RxJS pour effectuer un travail asynchrone, puis nous créerons une application dans React Native en utilisant Redux et redux-observable.

Redux

Comme il se décrit sur GitHub, Redux est "un conteneur d'état prévisible pour les applications JavaScript". Il fournit à vos applications JavaScript un état global, en gardant l'état et les actions à l'écart des composants React.

Dans une application React typique sans Redux, nous devons transmettre les données du nœud racine aux enfants via properties ou props . Ce flux de données est gérable pour les petites applications, mais peut devenir très complexe à mesure que votre application se développe. Redux nous permet d'avoir des composants indépendants les uns des autres, nous pouvons donc l'utiliser comme une source unique de vérité.

Redux peut être utilisé dans React à l'aide react-redux , qui fournit des liaisons aux composants React pour lire les données de Redux et envoyer des actions pour mettre à jour l'état Redux.

Redux

Redux peut être décrit comme trois principes simples :

1. Source unique de vérité

L'état de l'ensemble de votre application est stocké dans un seul objet. Cet objet dans Redux est détenu par un magasin. Il devrait y avoir un seul magasin dans n'importe quelle application Redux.

 » console.log(store.getState()) « { user: {...}, todos: {...} }

Pour lire les données de Redux dans votre composant React, nous utilisons la fonction connect de react-redux . connect prend quatre arguments, tous facultatifs. Pour l'instant, nous allons nous concentrer sur le premier, appelé mapStateToProps .

 /* UserTile.js */ import { connect } from 'react-redux'; class UserTile extends React.Component { render() { return <p>{ this.props.user.name }</p> } } function mapStateToProps(state) { return { user: state.user } } export default connect(mapStateToProps)(UserTile)

Dans l'exemple ci-dessus, mapStateToProps reçoit l'état Redux global comme premier argument et renvoie un objet qui sera fusionné avec les accessoires passés à <UserTile /> par son composant parent.

2. L'état est en lecture seule

L'état Redux est en lecture seule pour les composants React, et la seule façon de changer l'état est d'émettre une action . Une action est un objet simple qui représente une intention de changer l'état. Chaque objet d'action doit avoir un champ de type et la valeur doit être une chaîne. En dehors de cela, le contenu de l'action dépend entièrement de vous, mais la plupart des applications suivent un format flux-standard-action, qui limite la structure d'une action à seulement quatre clés :

  1. type N'importe quel identificateur de chaîne pour une action. Chaque action doit avoir une action unique.
  2. payload Données facultatives pour toute action. Il peut être de n'importe quel moment et contient des informations sur l'action.
  3. error Toute propriété booléenne facultative définie sur true si l'action représente une erreur. Ceci est analogue à une Promise. string identifiant de Promise. string pour une action. Chaque action doit avoir une action unique. Par convention, lorsque error est true , la payload doit être un objet d'erreur.
  4. meta Meta peut être n'importe quel type de valeur. Il est destiné à toute information supplémentaire qui ne fait pas partie de la charge utile.

Voici deux exemples d'actions :

 store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });

3. L'état est modifié avec des fonctions pures

L'état global de Redux est modifié à l'aide de fonctions pures appelées réducteurs. Un réducteur prend l'état et l'action précédents et renvoie l'état suivant. Le réducteur crée un nouvel objet d'état au lieu de muter celui existant. Selon la taille de l'application, un magasin Redux peut avoir un seul réducteur ou plusieurs réducteurs.

 /* store.js */ import { combineReducers, createStore } from 'redux' function user(state = {}, action) { switch (action.type) { case 'GET_USER_SUCCESS': return action.payload.user default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO_SUCCESS': return [ ...state, { id: uuid(), // a random uuid generator function text: action.text, completed: false } ] case 'COMPLETE_TODO_SUCCESS': return state.map(todo => { if (todo.id === action.id) { return { ...todo, completed: true } } return todo }) default: return state } } const rootReducer = combineReducers({ user, todos }) const store = createStore(rootReducer)

Semblable à la lecture de l'état, nous pouvons utiliser une fonction de connect pour envoyer des actions.

 /* UserProfile.js */ class Profile extends React.Component { handleSave(user) { this.props.updateUser(user); } } function mapDispatchToProps(dispatch) { return ({ updateUser: (user) => dispatch({ type: 'GET_USER_SUCCESS', user, }), }) } export default connect(mapStateToProps, mapDispatchToProps)(Profile);

RxJS

RxJS

Programmation réactive

La programmation réactive est un paradigme de programmation déclarative qui traite du flux de données en « flux » et de sa propagation et de ses modifications. RxJS, une bibliothèque de programmation réactive en JavaScript, a un concept d' observables , qui sont des flux de données auxquels un observateur peut s'abonner , et cet observateur reçoit des données au fil du temps.

Un observateur d'un observable est un objet avec trois fonctions : next , error et complete . Toutes ces fonctions sont facultatives.

 observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })

La fonction .subscribe peut également avoir trois fonctions au lieu d'un objet.

 observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )

Nous pouvons créer un nouvel observable en créant un objet d'un observable , en passant dans une fonction qui reçoit un abonné alias observer. L'abonné dispose de trois méthodes : next , error et complete . L'abonné peut appeler next avec une valeur autant de fois que nécessaire, et complete ou error à la fin. Après avoir appelé complete ou error , l'observable ne poussera aucune valeur dans le flux.

 import { Observable } from 'rxjs' const observable$ = new Observable(function subscribe(subscriber) { const intervalId = setInterval(() => { subscriber.next('hi'); subscriber.complete() clearInterval(intervalId); }, 1000); }); observable$.subscribe( value => console.log(`Value is ${value}`), err => console.log(err) )

L'exemple ci-dessus affichera Value is hi après 1000 millisecondes.

Créer un observable manuellement à chaque fois peut devenir verbeux et fastidieux. Par conséquent, RxJS a de nombreuses fonctions pour créer un observable. Certains des plus couramment utilisés sont of , from et ajax .

de

of prend une séquence de valeurs et la convertit en un flux :

 import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World

à partir de

from convertit presque n'importe quoi en un flux de valeurs :

 import { from } from 'rxjs' from([1, 2, 3]).subscribe(console.log) // 1 2 3 from(new Promise.resolve('Hello World')).subscribe(console.log) // 'Hello World' from(fibonacciGenerator).subscribe(console.log) // 1 1 2 3 5 8 13 21 ...

ajax

ajax prend une URL de chaîne ou crée un observable qui fait une requête HTTP. ajax a une fonction ajax.getJSON , qui renvoie uniquement l'objet de réponse imbriqué de l'appel AJAX sans aucune autre propriété renvoyée par ajax() :

 import { ajax } from 'rxjs/ajax' ajax('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {request, response: {userId, id, title, completed}, responseType, status} ajax.getJSON('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {userId, id, title, completed} ajax({ url, method, headers, body }).subscribe(console.log) // {...}

Il existe de nombreuses autres façons de créer un observable (vous pouvez voir la liste complète ici).

Les opérateurs

Les opérateurs sont une véritable centrale électrique de RxJS, qui dispose d'un opérateur pour presque tout ce dont vous aurez besoin. Depuis RxJS 6, les opérateurs ne sont plus des méthodes sur l'objet observable mais de pures fonctions appliquées sur l'observable à l'aide d'une méthode .pipe .

carte

map prend une fonction à argument unique et applique une projection sur chaque élément du flux :

 import { of } from 'rxjs' import { map } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i=> i * 2) ).subscribe(console.log) // 2, 4, 6, 8, 10 

Carte

filtre

filter prend un seul argument et supprime les valeurs du flux qui renvoient false pour la fonction donnée :

 import { of } from 'rxjs' import { map, filter } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i => i * i), filter(i => i % 2 === 0) ).subscribe(console.log) // 4, 16 

Filtre

flatMap

L'opérateur flatMap prend une fonction qui mappe chaque élément de la vapeur dans un autre flux et aplatit toutes les valeurs de ces flux :

 import { of } from 'rxjs' import { ajax } from 'rxjs/ajax' import { flatMap } from 'rxjs/operators' of(1, 2, 3).pipe( flatMap(page => ajax.toJSON(`https://example.com/blog?size=2&page=${page}`)), ).subscribe(console.log) // [ { blog 1 }, { blog 2 }, { blog 3 }, { blog 4 }, { blog 5 }, { blog 6 } ] 

FlatMap

fusionner

merge fusionne les éléments de deux flux dans l'ordre dans lequel ils arrivent :

 import { interval, merge } from 'rxjs' import { pipe, take, mapTo } from 'rxjs/operators' merge( interval(150).pipe(take(5), mapTo('A')), interval(250).pipe(take(5), mapTo('B')) ).subscribe(console.log) // ABAABAABBB 

Fusionner

Une liste complète des opérateurs est disponible ici.

Redux-Observable

Redux-Observable

De par leur conception, toutes les actions dans Redux sont synchrones. Redux-observable est un middleware pour Redux qui utilise des flux observables pour effectuer un travail asynchrone, puis envoie une autre action dans Redux avec le résultat de ce travail asynchrone.

Redux-observable est basé sur l'idée d' Epics . Une épopée est une fonction qui prend un flux d'actions, et éventuellement un flux d'état et renvoie un flux d'actions.

fonction (action$ : Observable , état$ : ÉtatObservable ): Observable ;

Par convention, chaque variable qui est un flux (_aka _observable ) se termine par un $ . Avant de pouvoir utiliser redux-observable, nous devons l'ajouter en tant que middleware dans notre magasin. Étant donné que les épopées sont des flux d'observables et que chaque action sortant de ce flux est renvoyée dans le flux, le retour de la même action entraînera une boucle infinie.

 const epic = action$ => action$.pipe( filter(action => action.type === 'FOO'), mapTo({ type: 'BAR' }) // not changing the type of action returned // will also result in an infinite loop ) // or import { ofType } from 'redux-observable' const epic = action$ => action$.pipe( ofType('FOO'), mapTo({ type: BAZ' }) )

Considérez cette architecture réactive comme un système de tuyaux où la sortie de chaque tuyau est renvoyée dans chaque tuyau, y compris lui-même, ainsi que dans les réducteurs de Redux. Ce sont les filtres au-dessus de ces tuyaux qui décident de ce qui entre et de ce qui est bloqué.

Voyons comment une épopée de ping-pong fonctionnerait. Il prend un ping, l'envoie au serveur - et une fois la demande terminée - renvoie un pong à l'application.

 const pingEpic = action$ => action$.pipe( ofType('PING'), flatMap(action => ajax('https://example.com/pinger')), mapTo({ type: 'PONG' }) ) Now, we are going to update our original todo store by adding epics and retrieving users. import { combineReducers, createStore } from 'redux' import { ofType, combineEpics, createEpicMiddleware } from 'redux-observable'; import { map, flatMap } from 'rxjs/operators' import { ajax } from 'rxjs/ajax' // ... /* user and todos reducers defined as above */ const rootReducer = combineReducers({ user, todos }) const epicMiddleware = createEpicMiddleware(); const userEpic = action$ => action$.pipe( ofType('GET_USER'), flatMap(() => ajax.getJSON('https://foo.bar.com/get-user')), map(user => ({ type: 'GET_USER_SUCCESS', payload: user })) ) const addTodoEpic = action$ => action$.pipe( ofType('ADD_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/add-todo', method: 'POST', body: { text: action.payload } })), map(data => data.response), map(todo => ({ type: 'ADD_TODO_SUCCESS', payload: todo })) ) const completeTodoEpic = action$ => action$.pipe( ofType('COMPLETE_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/complete-todo', method: 'POST', body: { id: action.payload } })), map(data => data.response), map(todo => ({ type: 'COMPLEE_TODO_SUCCESS', payload: todo })) ) const rootEpic = combineEpics(userEpic, addTodoEpic, completeTodoEpic) const store = createStore(rootReducer, applyMiddleware(epicMiddleware)) epicMiddleware.run(rootEpic);

_Important : les épopées sont comme n'importe quel autre flux observable dans RxJS. Ils peuvent se retrouver dans un état complet ou d'erreur. Après cet état, l'épopée et votre application cesseront de fonctionner. Vous devez donc détecter toutes les erreurs potentielles dans la vapeur. Vous pouvez utiliser l'opérateur __catchError__ pour cela. Plus d'informations : Gestion des erreurs dans redux-observable .

Une application Todo réactive

Avec quelques interfaces utilisateur ajoutées, une application de démonstration (minimale) ressemble à ceci :

Une application Todo réactive
Le code source de cette application est disponible sur Github. Essayez le projet sur Expo ou scannez le code QR ci-dessus dans l'application Expo.

Un tutoriel sur React, Redux et RxJS résumé

Nous avons appris ce que sont les applications réactives. Nous avons également découvert Redux, RxJS et redux-observable, et même créé une application Todo réactive dans Expo avec React Native. Pour les développeurs React et React Native, les tendances actuelles offrent des options de gestion d'état très puissantes.

Encore une fois, le code source de cette application est sur GitHub. N'hésitez pas à partager vos réflexions sur la gestion de l'état des applications réactives dans les commentaires ci-dessous.