Создание реактивных приложений с помощью Redux, RxJS и Redux-Observable в React Native

Опубликовано: 2022-03-11

В растущей экосистеме многофункциональных и мощных веб-приложений и мобильных приложений требуется управлять все большим количеством состояний, таких как текущий пользователь, список загруженных элементов, состояние загрузки, ошибки и многое другое. Redux — одно из решений этой проблемы, сохраняя состояние в глобальном объекте.

Одним из ограничений Redux является то, что он не поддерживает асинхронное поведение из коробки. Одним из решений для этого является redux-observable , основанный на RxJS, мощной библиотеке для реактивного программирования на JavaScript. RxJS — это реализация ReactiveX, API для реактивного программирования, разработанного Microsoft. ReactiveX сочетает в себе некоторые из самых мощных функций реактивной парадигмы, функционального программирования, шаблона наблюдателя и шаблона итератора.

В этом уроке мы узнаем о Redux и его использовании с React. Мы также рассмотрим реактивное программирование с использованием RxJS и то, как оно может сделать утомительную и сложную асинхронную работу очень простой.

Наконец, мы изучим redux-observable, библиотеку, которая использует RxJS для выполнения асинхронной работы, а затем создадим приложение в React Native, используя Redux и redux-observable.

Редукс

Как описывает себя на GitHub, Redux — это «контейнер с предсказуемым состоянием для приложений JavaScript». Он предоставляет вашим приложениям JavaScript глобальное состояние, сохраняя состояние и действия отдельно от компонентов React.

В типичном приложении React без Redux мы должны передавать данные от корневого узла дочерним элементам через свойства или props . Этот поток данных управляем для небольших приложений, но может стать очень сложным по мере роста вашего приложения. Redux позволяет нам иметь компоненты, независимые друг от друга, поэтому мы можем использовать его как единый источник правды.

Redux можно использовать в React с помощью react-redux , который предоставляет привязки для компонентов React для чтения данных из Redux и отправки действий для обновления состояния Redux.

Редукс

Redux можно описать как три простых принципа:

1. Единственный источник правды

Состояние всего вашего приложения хранится в одном объекте. Этот объект в Redux хранится в хранилище. В любом приложении Redux должен быть один магазин.

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

Чтобы прочитать данные из Redux в вашем компоненте React, мы используем функцию connect из react-redux . connect принимает четыре аргумента, все из которых являются необязательными. Сейчас мы сосредоточимся на первом, называемом 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)

В приведенном выше примере mapStateToProps получает глобальное состояние Redux в качестве своего первого аргумента и возвращает объект, который будет объединен с реквизитами, переданными <UserTile /> его родительским компонентом.

2. Состояние доступно только для чтения

Состояние Redux доступно только для чтения для компонентов React, и единственный способ изменить состояние — выполнить действие . Действие — это простой объект, представляющий намерение изменить состояние. Каждый объект действия должен иметь поле type , а значение должно быть строкой. Кроме этого, содержание действия полностью зависит от вас, но большинство приложений следуют стандартному формату действия, который ограничивает структуру действия только четырьмя клавишами:

  1. type Любой строковый идентификатор действия. Каждое действие должно иметь уникальное действие.
  2. payload Необязательные данные для любого действия. Он может быть любого времени и содержит информацию о действии.
  3. error Любое необязательное логическое свойство, установленное в значение true, если действие представляет собой ошибку. Это аналогично отклоненному Promise. string Promise. string идентификатор действия. Каждое действие должно иметь уникальное действие. По соглашению, когда error имеет значение true , payload должна быть объектом ошибки.
  4. meta Мета может быть значением любого типа. Он предназначен для любой дополнительной информации, которая не является частью полезной нагрузки.

Вот два примера действий:

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

3. Состояние изменяется с помощью чистых функций

Глобальное состояние Redux изменяется с помощью чистых функций, называемых редюсерами. Редуктор принимает предыдущее состояние и действие и возвращает следующее состояние. Редюсер создает новый объект состояния вместо изменения существующего. В зависимости от размера приложения в магазине Redux может быть один или несколько редьюсеров.

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

Подобно чтению из состояния, мы можем использовать функцию connect для отправки действий.

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

Реактивное программирование

Реактивное программирование — это парадигма декларативного программирования, которая имеет дело с потоком данных в « потоках », а также с их распространением и изменениями. RxJS, библиотека для реактивного программирования на JavaScript, имеет концепцию observables , которые представляют собой потоки данных, на которые может подписаться наблюдатель, и этому наблюдателю доставляются данные с течением времени.

Наблюдатель наблюдаемого — это объект с тремя функциями: next , error и complete . Все эти функции являются необязательными.

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

Функция .subscribe также может иметь три функции вместо объекта.

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

Мы можем создать новую наблюдаемую, создав объект observable , передав функцию, которая получает подписчика, известного как наблюдатель. У подписчика есть три метода: next , error и complete . Подписчик может вызвать next со значением столько раз, сколько требуется, и complete или error в конце. После вызова complete или error наблюдаемый объект не будет передавать какое-либо значение вниз по потоку.

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

В приведенном выше примере Value is hi будет напечатано через 1000 миллисекунд.

Создание наблюдаемого вручную каждый раз может стать многословным и утомительным. Поэтому в RxJS есть много функций для создания наблюдаемых. Некоторые из наиболее часто используемых — of , from и ajax .

из

of принимает последовательность значений и преобразует ее в поток:

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

от

from преобразует почти что угодно в поток значений:

 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 принимает строковый URL-адрес или создает наблюдаемый объект, который выполняет HTTP-запрос. ajax имеет функцию ajax.getJSON , которая возвращает только вложенный объект ответа из вызова AJAX без каких-либо других свойств, возвращаемых 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) // {...}

Есть еще много способов сделать observable (полный список можно посмотреть здесь).

Операторы

Операторы — это настоящая электростанция RxJS, у которой есть оператор практически для всего, что вам нужно. Начиная с RxJS 6, операторы — это не методы наблюдаемого объекта, а чистые функции, применяемые к наблюдаемому с использованием метода .pipe .

карта

map принимает функцию с одним аргументом и применяет проекцию к каждому элементу в потоке:

 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 

карта

фильтр

filter принимает один аргумент и удаляет из потока значения, возвращающие false для данной функции:

 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 

Фильтр

квартираКарта

Оператор flatMap использует функцию, которая отображает каждый элемент потока в другой поток и сглаживает все значения этих потоков:

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

Плоская карта

сливаться

merge объединяет элементы из двух потоков в порядке их поступления:

 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 

Объединить

Полный список операторов доступен здесь.

Редукс-наблюдаемый

Редукс-наблюдаемый

По замыслу все действия в Redux синхронны. Redux-observable — это промежуточное ПО для Redux, которое использует наблюдаемые потоки для выполнения асинхронной работы, а затем отправляет другое действие в Redux с результатом этой асинхронной работы.

Redux-observable основан на идее Epics . Эпопея — это функция, которая принимает поток действий и, возможно, поток состояния и возвращает поток действий.

функция (action$: Observable , состояние$: StateObservable ): наблюдаемый ;

По соглашению каждая переменная, являющаяся потоком (_aka _observable ), заканчивается символом $ . Прежде чем мы сможем использовать redux-observable, мы должны добавить его в качестве промежуточного программного обеспечения в наш магазин. Поскольку эпики — это потоки наблюдаемых, и каждое действие, выходящее из этого потока, передается обратно в поток, возврат того же действия приведет к бесконечному циклу.

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

Думайте об этой реактивной архитектуре как о системе каналов, в которой выходные данные каждого канала возвращаются в каждый канал, включая его самого, а также в редукторы Redux. Именно фильтры над этими трубами решают, что входит, а что блокируется.

Давайте посмотрим, как будет работать эпос о пинг-понге. Он принимает пинг, отправляет его на сервер, а после завершения запроса отправляет пинг обратно в приложение.

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

_Важно: Эпики такие же, как и любые другие наблюдаемые потоки в RxJS. Они могут оказаться в состоянии завершения или ошибки. После этого состояния эпик и ваше приложение перестанут работать. Так что вы должны ловить каждую потенциальную ошибку в стиме. Для этого вы можете использовать оператор __catchError__ . Дополнительная информация: Обработка ошибок в redux-observable .

Реактивное приложение Todo

С добавлением некоторого пользовательского интерфейса (минимальное) демонстрационное приложение выглядит примерно так:

Реактивное приложение Todo
Исходный код этого приложения доступен на Github. Попробуйте проект на Expo или отсканируйте приведенный выше QR-код в приложении Expo.

Резюме учебника по React, Redux и RxJS

Мы узнали, что такое реактивные приложения. Мы также узнали о Redux, RxJS и redux-observable и даже создали реактивное приложение Todo в Expo с помощью React Native. Для разработчиков React и React Native современные тенденции предлагают несколько очень мощных вариантов управления состоянием.

И снова исходный код этого приложения находится на GitHub. Не стесняйтесь делиться своими мыслями об управлении состоянием для реактивных приложений в комментариях ниже.