Creazione di app reattive con Redux, RxJS e Redux-Observable in React Native
Pubblicato: 2022-03-11Nel crescente ecosistema di app Web e mobili ricche e potenti, c'è sempre più stato da gestire, come utente corrente, elenco di elementi caricati, stato di caricamento, errori e molto altro. Redux è una soluzione a questo problema mantenendo lo stato in un oggetto globale.
Una delle limitazioni di Redux è che non supporta il comportamento asincrono pronto all'uso. Una soluzione per questo è redux-observable
, che si basa su RxJS, una potente libreria per la programmazione reattiva in JavaScript. RxJS è un'implementazione di ReactiveX, un'API per la programmazione reattiva che ha avuto origine in Microsoft. ReactiveX combina alcune delle caratteristiche più potenti del paradigma reattivo, la programmazione funzionale, il pattern osservatore e il pattern iteratore.
In questo tutorial, impareremo Redux e il suo utilizzo con React. Esploreremo anche la programmazione reattiva usando RxJS e come può rendere molto semplice il lavoro asincrono noioso e complesso.
Infine, impareremo redux-observable, una libreria che sfrutta RxJS per svolgere un lavoro asincrono, e quindi costruiremo un'applicazione in React Native usando Redux e redux-observable.
Redux
Come si descrive su GitHub, Redux è "un contenitore di stato prevedibile per app JavaScript". Fornisce alle tue app JavaScript uno stato globale, mantenendo lo stato e le azioni lontano dai componenti di React.
In una tipica applicazione React senza Redux, dobbiamo passare i dati dal nodo radice ai figli tramite proprietà o props
. Questo flusso di dati è gestibile per piccole applicazioni, ma può diventare molto complesso man mano che l'applicazione cresce. Redux ci consente di avere componenti indipendenti l'uno dall'altro, quindi possiamo usarlo come un'unica fonte di verità.
Redux può essere utilizzato in React usando react-redux
, che fornisce collegamenti ai componenti di React per leggere i dati da Redux e inviare azioni per aggiornare lo stato Redux.
Redux può essere descritto come tre semplici principi:
1. Unica fonte di verità
Lo stato dell'intera applicazione è archiviato in un unico oggetto. Questo oggetto in Redux è detenuto da un negozio. Dovrebbe esserci un unico negozio in qualsiasi app Redux.
» console.log(store.getState()) « { user: {...}, todos: {...} }
Per leggere i dati da Redux nel tuo componente React, utilizziamo la funzione di connect
da react-redux
. connect
accetta quattro argomenti, tutti facoltativi. Per ora, ci concentreremo sul primo, chiamato 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)
Nell'esempio precedente, mapStateToProps
riceve lo stato Redux globale come primo argomento e restituisce un oggetto che verrà unito agli oggetti di scena passati a <UserTile />
dal suo componente padre.
2. Lo stato è di sola lettura
Lo stato Redux è di sola lettura per i componenti React e l'unico modo per modificare lo stato è emettere un'azione . Un'azione è un semplice oggetto che rappresenta l'intenzione di cambiare lo stato. Ogni oggetto azione deve avere un campo type
e il valore deve essere una stringa. A parte questo, il contenuto dell'azione dipende totalmente da te, ma la maggior parte delle app segue un formato di azione standard di flusso, che limita la struttura di un'azione a soli quattro tasti:
-
type
Qualsiasi identificatore di stringa per un'azione. Ogni azione deve avere un'azione unica. -
payload
Dati facoltativi per qualsiasi azione. Può essere di qualsiasi momento e contiene informazioni sull'azione. -
error
Qualsiasi proprietà booleana facoltativa impostata su true se l'azione rappresenta un errore. Questo è analogo a unaPromise. string
identificatore diPromise. string
per un'azione. Ogni azione deve avere un'azione unica. Per convenzione, quandoerror
ètrue
, ilpayload
dovrebbe essere un oggetto di errore. -
meta
Meta può essere qualsiasi tipo di valore. È destinato a qualsiasi informazione aggiuntiva che non fa parte del carico utile.
Ecco due esempi di azioni:
store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });
3. Lo stato è cambiato con le funzioni pure
Lo stato Redux globale viene modificato utilizzando funzioni pure chiamate riduttori. Un riduttore prende lo stato e l'azione precedenti e restituisce lo stato successivo. Il riduttore crea un nuovo oggetto di stato invece di mutare quello esistente. A seconda delle dimensioni dell'app, un negozio Redux può avere un singolo riduttore o più riduttori.
/* 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)
Simile alla lettura dallo stato, possiamo usare una funzione di connect
per inviare le azioni.
/* 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
Programmazione reattiva
La programmazione reattiva è un paradigma di programmazione dichiarativo che si occupa del flusso di dati nei " flussi " e della sua propagazione e modifica. RxJS, una libreria per la programmazione reattiva in JavaScript, ha un concetto di osservabili , che sono flussi di dati a cui un osservatore può iscriversi e a questo osservatore vengono forniti dati nel tempo.
Un osservatore di un osservabile è un oggetto con tre funzioni: next
, error
e complete
. Tutte queste funzioni sono opzionali.
observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })
La funzione .subscribe
può anche avere tre funzioni invece di un oggetto.
observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )
Possiamo creare un nuovo osservabile creando un oggetto di un observable
, passando una funzione che riceve un abbonato alias osservatore. L'abbonato ha tre metodi: next
, error
e complete
. L'abbonato può chiamare successivo con un valore tutte le volte necessarie e alla fine complete
o error
. Dopo aver chiamato complete
o error
, l'osservabile non eseguirà il push di alcun valore nel flusso.
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'esempio precedente stamperà Value is hi
dopo 1000 millisecondi.
Creare un osservabile manualmente ogni volta può diventare prolisso e noioso. Pertanto, RxJS ha molte funzioni per creare un osservabile. Alcuni dei più comunemente usati sono of
, from
e ajax
.
di
of
prende una sequenza di valori e la converte in un flusso:

import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World
da
from
converte quasi tutto in un flusso di valori:
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
prende una stringa URL o crea un osservabile che effettua una richiesta HTTP. ajax
ha una funzione ajax.getJSON
, che restituisce solo l'oggetto risposta nidificato dalla chiamata AJAX senza altre proprietà restituite da 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) // {...}
Ci sono molti altri modi per rendere osservabile (puoi vedere l'elenco completo qui).
Operatori
Gli operatori sono una vera potenza di RxJS, che ha un operatore per quasi tutto ciò di cui avrai bisogno. Da RxJS 6, gli operatori non sono metodi sull'oggetto osservabile ma pure funzioni applicate sull'osservabile utilizzando un metodo .pipe
.
carta geografica
map
prende una singola funzione argomento e applica una proiezione su ogni elemento nel flusso:
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
filtro
filter
accetta un singolo argomento e rimuove i valori dal flusso che restituiscono false per la funzione data:
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
mappa piatta
L'operatore flatMap
prende una funzione che mappa ogni elemento nel vapore in un altro flusso e appiattisce tutti i valori di questi flussi:
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 } ]
unire
merge
unisce gli elementi di due flussi nell'ordine in cui arrivano:
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
L'elenco completo degli operatori è disponibile qui.
Redux-osservabile
In base alla progettazione, tutte le azioni in Redux sono sincrone. Redux-observable è un middleware per Redux che utilizza flussi osservabili per eseguire un lavoro asincrono e quindi inviare un'altra azione in Redux con il risultato di quel lavoro asincrono.
Redux-observable si basa sull'idea di Epics . Un'epica è una funzione che accetta un flusso di azioni e, facoltativamente, un flusso di stato e restituisce un flusso di azioni.
funzione (azione$: Osservabile
Per convenzione, ogni variabile che è un flusso (_aka _observable
) termina con $
. Prima di poter utilizzare redux-observable, dobbiamo aggiungerlo come middleware nel nostro negozio. Poiché le epiche sono flussi di osservabili e ogni azione che esce da questo vapore viene convogliata di nuovo nel flusso, restituire la stessa azione risulterà in un ciclo infinito.
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' }) )
Pensa a questa architettura reattiva come a un sistema di tubi in cui l'uscita di ogni tubo reimmette in ogni tubo, incluso se stesso, e anche nei riduttori di Redux. Sono i filtri in cima a questi tubi che decidono cosa entra e cosa è bloccato.
Vediamo come funzionerebbe un'epopea di Ping-Pong. Prende un ping, lo invia al server e, al termine della richiesta, invia un pong all'app.
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);
_Importante: le epiche sono come qualsiasi altro flusso osservabile in RxJS. Possono finire in uno stato completo o di errore. Dopo questo stato, l'epica e la tua app smetteranno di funzionare. Quindi devi catturare ogni potenziale errore nel vapore. È possibile utilizzare l'operatore __catchError__
per questo. Ulteriori informazioni: Gestione degli errori in redux-observable .
Un'app Todo reattiva
Con l'aggiunta di un'interfaccia utente, un'app demo (minima) ha un aspetto simile a questo:
Un tutorial React, Redux e RxJS riassunto
Abbiamo imparato cosa sono le app reattive. Abbiamo anche imparato a conoscere Redux, RxJS e redux-observable e abbiamo persino creato un'app Todo reattiva in Expo con React Native. Per gli sviluppatori React e React Native, le tendenze attuali offrono alcune opzioni di gestione dello stato molto potenti.
Ancora una volta, il codice sorgente per questa app è su GitHub. Sentiti libero di condividere le tue opinioni sulla gestione dello stato per le app reattive nei commenti qui sotto.