Creazione di app reattive con Redux, RxJS e Redux-Observable in React Native

Pubblicato: 2022-03-11

Nel 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

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:

  1. type Qualsiasi identificatore di stringa per un'azione. Ogni azione deve avere un'azione unica.
  2. payload Dati facoltativi per qualsiasi azione. Può essere di qualsiasi momento e contiene informazioni sull'azione.
  3. error Qualsiasi proprietà booleana facoltativa impostata su true se l'azione rappresenta un errore. Questo è analogo a una Promise. string identificatore di Promise. string per un'azione. Ogni azione deve avere un'azione unica. Per convenzione, quando error è true , il payload dovrebbe essere un oggetto di errore.
  4. 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

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 

Carta geografica

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 

Filtro

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 } ] 

Mappa piatta

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 

Unisci

L'elenco completo degli operatori è disponibile qui.

Redux-osservabile

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 , state$: StateObservable ): 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'app Todo reattiva
Il codice sorgente di questa app è disponibile su Github. Prova il progetto su Expo o scansiona il codice QR sopra nell'app Expo.

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.