Tworzenie aplikacji reaktywnych za pomocą Redux, RxJS i Redux-Observable w React Native
Opublikowany: 2022-03-11W rozwijającym się ekosystemie bogatych i wydajnych aplikacji internetowych i mobilnych istnieje coraz więcej stanów do zarządzania, takich jak bieżący użytkownik, lista załadowanych elementów, stan ładowania, błędy i wiele więcej. Redux jest jednym z rozwiązań tego problemu, utrzymując stan w obiekcie globalnym.
Jednym z ograniczeń Redux jest to, że po wyjęciu z pudełka nie obsługuje on zachowania asynchronicznego. Jednym z rozwiązań tego problemu jest redux-observable
, który jest oparty na RxJS, potężnej bibliotece do reaktywnego programowania w JavaScript. RxJS to implementacja ReactiveX, API do programowania reaktywnego, które powstało w Microsoft. ReactiveX łączy w sobie niektóre z najpotężniejszych cech paradygmatu reaktywnego, programowania funkcjonalnego, wzorca obserwatora i wzorca iteratora.
W tym samouczku dowiemy się o Redux i jego wykorzystaniu w React. Przyjrzymy się również programowaniu reaktywnemu przy użyciu RxJS i temu, jak może bardzo uprościć żmudną i złożoną pracę asynchroniczną.
Na koniec nauczymy się biblioteki redux-observable, która wykorzystuje RxJS do wykonywania asynchronicznej pracy, a następnie zbudujemy aplikację w React Native przy użyciu Redux i redux-observable.
Redux
Jak sam siebie opisuje na GitHub, Redux jest „przewidywalnym kontenerem stanu dla aplikacji JavaScript”. Zapewnia aplikacjom JavaScript globalny stan, utrzymując stan i akcje z dala od komponentów React.
W typowej aplikacji Reacta bez Redux, musimy przekazać dane z węzła głównego do dzieci za pośrednictwem właściwości lub props
. Ten przepływ danych jest łatwy do zarządzania w przypadku małych aplikacji, ale może stać się naprawdę złożony w miarę rozwoju aplikacji. Redux pozwala nam mieć komponenty niezależne od siebie, dzięki czemu możemy używać go jako pojedynczego źródła prawdy.
Redux może być używany w React za pomocą React react-redux
, który zapewnia powiązania dla komponentów React do odczytywania danych z Redux i wysyłania akcji w celu aktualizacji stanu Redux.
Redux można opisać jako trzy proste zasady:
1. Pojedyncze źródło prawdy
Stan całej aplikacji jest przechowywany w jednym obiekcie. Ten obiekt w Redux jest utrzymywany przez sklep. W każdej aplikacji Redux powinien znajdować się jeden sklep.
» console.log(store.getState()) « { user: {...}, todos: {...} }
Aby odczytać dane z Redux w Twoim komponencie React, używamy funkcji connect
z react-redux
. connect
przyjmuje cztery argumenty, z których wszystkie są opcjonalne. Na razie skupimy się na pierwszym, zwanym 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)
W powyższym przykładzie mapStateToProps
odbiera globalny stan Redux jako swój pierwszy argument i zwraca obiekt, który zostanie scalony z właściwościami przekazanymi do <UserTile />
przez jego komponent nadrzędny.
2. Stan jest tylko do odczytu
Stan Redux jest tylko do odczytu dla komponentów React, a jedynym sposobem na zmianę stanu jest wyemitowanie akcji . Akcja to zwykły obiekt, który reprezentuje zamiar zmiany stanu. Każdy obiekt akcji musi mieć pole type
, a wartość musi być ciągiem. Poza tym zawartość akcji zależy wyłącznie od Ciebie, ale większość aplikacji korzysta z formatu standardowego strumienia, który ogranicza strukturę akcji tylko do czterech klawiszy:
-
type
dowolny identyfikator ciągu dla akcji. Każda akcja musi mieć unikalną akcję. -
payload
Dane opcjonalne dla dowolnej akcji. Może mieć dowolny czas i zawierać informacje o akcji. -
error
Dowolna opcjonalna właściwość logiczna ustawiona na true, jeśli akcja reprezentuje błąd. Jest to analogiczne do odrzuconejPromise. string
identyfikatorPromise. string
dla akcji. Każda akcja musi mieć unikalną akcję. Zgodnie z konwencją, gdyerror
istrue
,payload
powinien być obiektem błędu. -
meta
Meta może być wartością dowolnego typu. Jest przeznaczony na wszelkie dodatkowe informacje, które nie są częścią ładunku.
Oto dwa przykłady działań:
store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });
3. Stan zmienia się za pomocą czystych funkcji
Globalny stan Redux jest zmieniany za pomocą czystych funkcji zwanych reduktorami. Reduktor przejmuje poprzedni stan i akcję i zwraca następny stan. Reduktor tworzy nowy obiekt stanu zamiast mutować istniejący. W zależności od rozmiaru aplikacji sklep Redux może mieć pojedynczy reduktor lub wiele reduktorów.
/* 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)
Podobnie jak w przypadku czytania ze stanu, możemy użyć funkcji connect
do akcji wysyłania.
/* 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
Programowanie reaktywne
Programowanie reaktywne to paradygmat programowania deklaratywnego, który zajmuje się przepływem danych w „ strumieniach ” oraz ich propagacją i zmianami. RxJS, biblioteka do programowania reaktywnego w JavaScript, ma koncepcję obserwowalnych , które są strumieniami danych, które obserwator może subskrybować , a ten obserwator otrzymuje dane w czasie.
Obserwatorem obserwowalnego jest obiekt z trzema funkcjami: next
, error
, i complete
. Wszystkie te funkcje są opcjonalne.
observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })
Funkcja .subscribe
może również mieć trzy funkcje zamiast obiektu.
observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )
Możemy stworzyć nowy obserwowalny, tworząc obiekt observable
, przekazując funkcję, która odbiera subskrybenta aka obserwatora. Subskrybent ma trzy metody: next
, error
i complete
. Abonent może zadzwonić dalej z wartością tyle razy, ile jest to wymagane, i complete
lub z error
na końcu. Po wywołaniu complete
lub error
obserwowalny nie wypchnie żadnej wartości w dół strumienia.
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) )
Powyższy przykład wyświetli Value is hi
po 1000 milisekund.
Tworzenie obserwowalnego ręcznie za każdym razem może stać się gadatliwe i nużące. Dlatego RxJS ma wiele funkcji do tworzenia obserwowalnego. Niektóre z najczęściej of
to , from
i ajax
.
z
of
pobiera sekwencję wartości i zamienia ją w strumień:
import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World
od
from
konwertuje prawie wszystko na strumień wartości:

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
przyjmuje adres URL ciągu lub tworzy obserwowalny, który tworzy żądanie HTTP. ajax
posiada funkcję ajax.getJSON
, która zwraca tylko zagnieżdżony obiekt odpowiedzi z wywołania AJAX bez żadnych innych właściwości zwracanych przez 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) // {...}
Istnieje wiele innych sposobów na uczynienie obserwowalnego (pełna lista znajduje się tutaj).
Operatorzy
Operatory to prawdziwa potęga RxJS, która ma operatora do prawie wszystkiego, czego potrzebujesz. Od RxJS 6, operatory nie są metodami na obserwowalnym obiekcie, ale czystymi funkcjami zastosowanymi na obserwowalnym za pomocą metody .pipe
.
mapa
map
przyjmuje funkcję pojedynczego argumentu i stosuje rzutowanie na każdy element w strumieniu:
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
filtr
filter
przyjmuje pojedynczy argument i usuwa ze strumienia wartości, które zwracają wartość false dla danej funkcji:
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
płaskaMapa
Operator flatMap
przyjmuje funkcję, która mapuje każdy element pary do innego strumienia i spłaszcza wszystkie wartości tych strumieni:
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 } ]
łączyć
merge
scala elementy z dwóch strumieni w kolejności ich przybycia:
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
Pełna lista operatorów dostępna jest tutaj.
Redux-obserwowalny
Z założenia wszystkie akcje w Redux są synchroniczne. Redux-observable to oprogramowanie pośredniczące dla Redux, które używa obserwowalnych strumieni do wykonywania pracy asynchronicznej, a następnie wysyła inną akcję w Redux z wynikiem tej pracy asynchronicznej.
Redux-observable opiera się na idei Epics . Epopeja to funkcja, która pobiera strumień działań i opcjonalnie strumień stanu i zwraca strumień działań.
funkcja (akcja $: Obserwowalna
Zgodnie z konwencją każda zmienna będąca strumieniem (_aka _observable
) kończy się znakiem $
. Zanim będziemy mogli używać redux-observable, musimy dodać go jako oprogramowanie pośredniczące w naszym sklepie. Ponieważ eposy są strumieniami obserwowalnych, a każda akcja wychodząca z tej pary jest przesyłana z powrotem do strumienia, zwrócenie tej samej akcji spowoduje nieskończoną pętlę.
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' }) )
Pomyśl o tej reaktywnej architekturze jako o systemie potoków, w którym wyjście z każdego potoku trafia z powrotem do każdego potoku, w tym do niego samego, a także do reduktorów Redux. To filtry na górze tych rur decydują o tym, co wchodzi, a co jest blokowane.
Zobaczmy, jak działa epopeja ping-ponga. Pobiera ping, wysyła go na serwer — a po zakończeniu żądania — wysyła pong z powrotem do aplikacji.
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);
_Ważne: Epiki są takie same jak inne obserwowalne strumienie w RxJS. Mogą skończyć w stanie kompletnym lub błędnym. Po tym stanie epopeja — i Twoja aplikacja — przestaną działać. Musisz więc wyłapać każdy potencjalny błąd w parze. W tym celu można użyć operatora __catchError__
. Więcej informacji: Obsługa błędów w redux-observable .
Reaktywna aplikacja Todo
Po dodaniu jakiegoś interfejsu użytkownika (minimalna) aplikacja demonstracyjna wygląda mniej więcej tak:
Podsumowanie samouczka React, Redux i RxJS
Dowiedzieliśmy się, czym są aplikacje reaktywne. Dowiedzieliśmy się również o Redux, RxJS i redux-observable, a nawet stworzyliśmy reaktywną aplikację Todo w Expo z React Native. Dla programistów React i React Native obecne trendy oferują bardzo rozbudowane opcje zarządzania stanem.
Po raz kolejny kod źródłowy tej aplikacji znajduje się na GitHub. Zachęcamy do podzielenia się swoimi przemyśleniami na temat zarządzania stanem aplikacji reaktywnych w komentarzach poniżej.