Erstellen reaktiver Apps mit Redux, RxJS und Redux-Observable in React Native

Veröffentlicht: 2022-03-11

In dem wachsenden Ökosystem aus reichhaltigen und leistungsstarken Web- und Mobil-Apps müssen immer mehr Status verwaltet werden, z. B. aktueller Benutzer, Liste der geladenen Elemente, Ladestatus, Fehler und vieles mehr. Redux ist eine Lösung für dieses Problem, indem der Zustand in einem globalen Objekt gehalten wird.

Eine der Einschränkungen von Redux besteht darin, dass es asynchrones Verhalten nicht standardmäßig unterstützt. Eine Lösung dafür ist redux-observable , das auf RxJS basiert, einer mächtigen Bibliothek für reaktive Programmierung in JavaScript. RxJS ist eine Implementierung von ReactiveX, einer API für reaktive Programmierung, die ihren Ursprung bei Microsoft hat. ReactiveX kombiniert einige der leistungsstärksten Funktionen des reaktiven Paradigmas, der funktionalen Programmierung, des Beobachtermusters und des Iteratormusters.

In diesem Tutorial lernen wir Redux und seine Verwendung mit React kennen. Wir werden auch die reaktive Programmierung mit RxJS untersuchen und wie sie mühsame und komplexe asynchrone Arbeit sehr einfach machen kann.

Schließlich lernen wir redux-observable kennen, eine Bibliothek, die RxJS nutzt, um asynchrone Arbeit zu leisten, und werden dann eine Anwendung in React Native mit Redux und redux-observable erstellen.

Redux

Wie es sich selbst auf GitHub beschreibt, ist Redux „ein vorhersagbarer Zustandscontainer für JavaScript-Apps“. Es stellt Ihren JavaScript-Apps einen globalen Status bereit und hält Status und Aktionen von React-Komponenten fern.

In einer typischen React-Anwendung ohne Redux müssen wir Daten vom Root-Knoten über Eigenschaften oder props an Kinder übergeben. Dieser Datenfluss ist für kleine Anwendungen überschaubar, kann jedoch sehr komplex werden, wenn Ihre Anwendung wächst. Redux ermöglicht es uns, voneinander unabhängige Komponenten zu haben, sodass wir es als Single Source of Truth verwenden können.

Redux kann in React mithilfe von „ react-redux “ verwendet werden, das Bindungen für React-Komponenten bereitstellt, um Daten aus Redux zu lesen und Aktionen zum Aktualisieren des Redux-Zustands zu senden.

Redux

Redux kann als drei einfache Prinzipien beschrieben werden:

1. Einzige Quelle der Wahrheit

Der Status Ihrer gesamten Anwendung wird in einem einzigen Objekt gespeichert. Dieses Objekt in Redux wird von einem Geschäft gehalten. In jeder Redux-App sollte es einen einzigen Store geben.

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

Um Daten von Redux in Ihre React-Komponente einzulesen, verwenden wir die connect -Funktion von React react-redux . connect nimmt vier Argumente entgegen, die alle optional sind. Im Moment konzentrieren wir uns auf den ersten namens 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)

Im obigen Beispiel empfängt mapStateToProps den globalen Redux-Zustand als erstes Argument und gibt ein Objekt zurück, das mit den Requisiten zusammengeführt wird, die von seiner übergeordneten Komponente an <UserTile /> werden.

2. Status ist schreibgeschützt

Der Redux-Zustand ist für React-Komponenten schreibgeschützt, und die einzige Möglichkeit, den Zustand zu ändern, besteht darin, eine Aktion auszugeben. Eine Aktion ist ein einfaches Objekt, das eine Absicht darstellt, den Zustand zu ändern. Jedes Aktionsobjekt muss ein type haben und der Wert muss eine Zeichenfolge sein. Abgesehen davon liegt der Inhalt der Aktion ganz bei Ihnen, aber die meisten Apps folgen einem Flux-Standard-Aktionsformat, das die Struktur einer Aktion auf nur vier Tasten beschränkt:

  1. type Beliebiger Zeichenfolgenbezeichner für eine Aktion. Jede Aktion muss eine eindeutige Aktion haben.
  2. payload Optionale Daten für jede Aktion. Es kann jederzeit sein und enthält Informationen über die Aktion.
  3. error Jede optionale boolesche Eigenschaft, die auf „true“ gesetzt ist, wenn die Aktion einen Fehler darstellt. Dies ist analog zu einem abgelehnten Promise. string Promise. string für eine Aktion. Jede Aktion muss eine eindeutige Aktion haben. Wenn error true ist, sollte die payload per Konvention ein Fehlerobjekt sein.
  4. meta Meta kann jede Art von Wert sein. Es ist für alle zusätzlichen Informationen gedacht, die nicht Teil der Nutzlast sind.

Hier sind zwei Beispiele für Aktionen:

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

3. Zustand wird mit reinen Funktionen geändert

Der globale Redux-Zustand wird mit reinen Funktionen namens Reducer geändert. Ein Reduzierer übernimmt den vorherigen Zustand und die vorherige Aktion und gibt den nächsten Zustand zurück. Der Reducer erstellt ein neues Zustandsobjekt, anstatt das vorhandene zu mutieren. Abhängig von der App-Größe kann ein Redux-Speicher einen einzelnen Reducer oder mehrere Reducer haben.

 /* 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)

Ähnlich wie beim Lesen aus dem Status können wir eine connect -Funktion verwenden, um Aktionen zu versenden.

 /* 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

Reaktive Programmierung

Die reaktive Programmierung ist ein deklaratives Programmierparadigma, das sich mit dem Datenfluss in „ Streams “ und mit seiner Ausbreitung und seinen Änderungen befasst. RxJS, eine Bibliothek für reaktive Programmierung in JavaScript, hat ein Konzept von Observables , bei denen es sich um Datenströme handelt, die ein Beobachter abonnieren kann, und diesem Beobachter werden im Laufe der Zeit Daten geliefert.

Ein Beobachter einer Observable ist ein Objekt mit drei Funktionen: next , error und complete . Alle diese Funktionen sind optional.

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

Die .subscribe Funktion kann anstelle eines Objekts auch drei Funktionen haben.

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

Wir können ein neues Observable erstellen, indem wir ein Objekt eines observable erstellen und eine Funktion übergeben, die einen Abonnenten, auch bekannt als Observer, empfängt. Der Abonnent hat drei Methoden: next , error und complete . Der Teilnehmer kann beliebig oft next mit einem Wert aufrufen und am Ende complete oder error . Nach dem Aufrufen von complete oder error wird das Observable keinen Wert in den Stream verschieben.

 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) )

Das obige Beispiel gibt Value is hi nach 1000 Millisekunden aus.

Jedes Mal manuell ein Observable zu erstellen, kann langatmig und mühsam werden. Daher hat RxJS viele Funktionen, um ein Observable zu erstellen. Einige der am häufigsten verwendeten sind of , from und ajax .

von

of nimmt eine Folge von Werten und wandelt sie in einen Stream um:

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

von

from wandelt fast alles in einen Strom von Werten um:

 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 nimmt eine Zeichenfolgen-URL oder erstellt ein Observable, das eine HTTP-Anfrage stellt. ajax hat eine Funktion ajax.getJSON , die nur das verschachtelte Antwortobjekt vom AJAX-Aufruf ohne andere von ajax() zurückgegebene Eigenschaften zurückgibt:

 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) // {...}

Es gibt viele weitere Möglichkeiten, ein Observable zu erstellen (die vollständige Liste finden Sie hier).

Betreiber

Operatoren sind ein echtes Kraftpaket von RxJS, das für fast alles, was Sie brauchen, einen Operator hat. Seit RxJS 6 sind Operatoren keine Methoden auf dem beobachtbaren Objekt, sondern reine Funktionen, die mit einer .pipe Methode auf das beobachtbare Objekt angewendet werden.

Karte

map nimmt eine einzelne Argumentfunktion und wendet eine Projektion auf jedes Element im Stream an:

 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 

Karte

Filter

filter nimmt ein einzelnes Argument und entfernt Werte aus dem Stream, die für die angegebene Funktion falsch zurückgeben:

 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 

Filter

flachKarte

flatMap Operator verwendet eine Funktion, die jedes Element im Steam einem anderen Stream zuordnet und alle Werte dieser Streams glättet:

 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

verschmelzen

merge führt Elemente aus zwei Streams in der Reihenfolge ihres Eintreffens zusammen:

 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 

Verschmelzen

Eine vollständige Liste der Betreiber finden Sie hier.

Redux-beobachtbar

Redux-beobachtbar

Per Design sind alle Aktionen in Redux synchron. Redux-observable ist eine Middleware für Redux, die beobachtbare Streams verwendet, um asynchrone Arbeit auszuführen, und dann eine andere Aktion in Redux mit dem Ergebnis dieser asynchronen Arbeit auslöst.

Redux-Observable basiert auf der Idee von Epics . Ein Epic ist eine Funktion, die einen Aktionsstrom und optional einen Zustandsstrom annimmt und einen Aktionsstrom zurückgibt.

Funktion (action$: Observable , state$: StateObservable ): Beobachtbar ;

Per Konvention endet jede Variable, die ein Stream (_aka _observable ) ist, mit einem $ . Bevor wir redux-observable verwenden können, müssen wir es als Middleware in unserem Store hinzufügen. Da Epen Ströme von Observablen sind und jede Aktion, die diesen Dampf verlässt, zurück in den Stream geleitet wird, führt die Rückgabe derselben Aktion zu einer Endlosschleife.

 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' }) )

Stellen Sie sich diese reaktive Architektur als ein System von Röhren vor, bei dem der Ausgang jeder Röhre in jede Röhre zurückgeführt wird, einschließlich sich selbst, und auch in die Reduzierer von Redux. Es sind die Filter oben auf diesen Rohren, die entscheiden, was hineingeht und was blockiert wird.

Mal sehen, wie ein Ping-Pong-Epos funktionieren würde. Es nimmt einen Ping entgegen, sendet ihn an den Server – und sendet nach Abschluss der Anfrage einen Pong zurück an die 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);

_Wichtig: Epics sind genau wie alle anderen beobachtbaren Streams in RxJS. Sie können in einem vollständigen oder fehlerhaften Zustand enden. Nach diesem Status funktionieren das Epic – und Ihre App – nicht mehr. Sie müssen also jeden potenziellen Fehler im Dampf abfangen. Sie können dafür den Operator __catchError__ verwenden. Weitere Informationen: Fehlerbehandlung in redux-observable .

Eine reaktive Todo-App

Mit einigen hinzugefügten UIs sieht eine (minimale) Demo-App etwa so aus:

Eine reaktive Todo-App
Der Quellcode für diese App ist auf Github verfügbar. Probieren Sie das Projekt auf Expo aus oder scannen Sie den QR-Code oben in der Expo-App.

Ein React-, Redux- und RxJS-Tutorial zusammengefasst

Wir haben gelernt, was reaktive Apps sind. Wir haben auch etwas über Redux, RxJS und redux-observable gelernt und sogar eine reaktive Todo-App in Expo mit React Native erstellt. Für Entwickler von React und React Native bieten die aktuellen Trends einige sehr leistungsstarke Statusverwaltungsoptionen.

Auch hier befindet sich der Quellcode für diese App auf GitHub. Fühlen Sie sich frei, Ihre Gedanken zur Zustandsverwaltung für reaktive Apps in den Kommentaren unten mitzuteilen.