Creación de aplicaciones reactivas con Redux, RxJS y Redux-Observable en React Native

Publicado: 2022-03-11

En el creciente ecosistema de aplicaciones web y móviles ricas y potentes, hay más y más estados para administrar, como el usuario actual, la lista de elementos cargados, el estado de carga, los errores y mucho más. Redux es una solución a este problema al mantener el estado en un objeto global.

Una de las limitaciones de Redux es que no admite el comportamiento asincrónico de forma inmediata. Una solución para esto es redux-observable , que se basa en RxJS, una poderosa biblioteca para programación reactiva en JavaScript. RxJS es una implementación de ReactiveX, una API para programación reactiva que se originó en Microsoft. ReactiveX combina algunas de las características más poderosas del paradigma reactivo, la programación funcional, el patrón de observador y el patrón de iterador.

En este tutorial, aprenderemos sobre Redux y su uso con React. También exploraremos la programación reactiva usando RxJS y cómo puede hacer que el trabajo asincrónico complejo y tedioso sea muy simple.

Finalmente, aprenderemos redux-observable, una biblioteca que aprovecha RxJS para realizar un trabajo asíncrono, y luego crearemos una aplicación en React Native usando Redux y redux-observable.

redux

Como se describe a sí mismo en GitHub, Redux es "un contenedor de estado predecible para aplicaciones de JavaScript". Proporciona a sus aplicaciones de JavaScript un estado global, manteniendo el estado y las acciones alejados de los componentes de React.

En una aplicación React típica sin Redux, tenemos que pasar datos desde el nodo raíz a los niños a través de propiedades o props . Este flujo de datos es manejable para aplicaciones pequeñas, pero puede volverse realmente complejo a medida que crece su aplicación. Redux nos permite tener componentes independientes entre sí, por lo que podemos usarlo como una única fuente de verdad.

Redux se puede usar en React usando react-redux , que proporciona enlaces para que los componentes de React lean datos de Redux y envíen acciones para actualizar el estado de Redux.

redux

Redux se puede describir como tres principios simples:

1. Fuente única de la verdad

El estado de toda su aplicación se almacena en un solo objeto. Este objeto en Redux está en manos de una tienda. Debería haber una sola tienda en cualquier aplicación Redux.

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

Para leer datos de Redux en su componente React, usamos la función de connect de react-redux . connect toma cuatro argumentos, todos los cuales son opcionales. Por ahora, nos centraremos en el primero, llamado 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)

En el ejemplo anterior, mapStateToProps recibe el estado global de Redux como su primer argumento y devuelve un objeto que se fusionará con los accesorios pasados ​​a <UserTile /> por su componente principal.

2. El estado es de solo lectura

El estado de Redux es de solo lectura para los componentes de React, y la única forma de cambiar el estado es emitir una acción . Una acción es un objeto simple que representa una intención de cambiar el estado. Cada objeto de acción debe tener un campo de type y el valor debe ser una cadena. Aparte de eso, el contenido de la acción depende totalmente de usted, pero la mayoría de las aplicaciones siguen un formato de acción estándar de flujo, que limita la estructura de una acción a solo cuatro teclas:

  1. type Cualquier identificador de cadena para una acción. Cada acción debe tener una acción única.
  2. payload Datos opcionales para cualquier acción. Puede ser de cualquier época y contiene información sobre la acción.
  3. error Cualquier propiedad booleana opcional establecida en verdadero si la acción representa un error. Esto es análogo a una Promise. string identificador de Promise. string para una acción. Cada acción debe tener una acción única. Por convención, cuando error es true , la payload debe ser un objeto de error.
  4. meta Meta puede ser cualquier tipo de valor. Está destinado a cualquier información adicional que no sea parte de la carga útil.

Aquí hay dos ejemplos de acciones:

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

3. El estado se cambia con funciones puras

El estado global de Redux se cambia usando funciones puras llamadas reductores. Un reductor toma el estado y la acción anteriores y devuelve el siguiente estado. El reductor crea un nuevo objeto de estado en lugar de mutar el existente. Según el tamaño de la aplicación, una tienda Redux puede tener un solo reductor o varios reductores.

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

Similar a la lectura del estado, podemos usar una función de connect para enviar acciones.

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

Programación reactiva

La programación reactiva es un paradigma de programación declarativa que se ocupa del flujo de datos en " flujos " y de su propagación y cambios. RxJS, una biblioteca para programación reactiva en JavaScript, tiene un concepto de observables , que son flujos de datos a los que un observador puede suscribirse , y este observador recibe datos a lo largo del tiempo.

Un observador de un observable es un objeto con tres funciones: next , error y complete . Todas estas funciones son opcionales.

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

La función .subscribe también puede tener tres funciones en lugar de un objeto.

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

Podemos crear un nuevo observable creando un objeto de un observable , pasando una función que recibe un suscriptor, también conocido como observador. El suscriptor tiene tres métodos: next , error y complete . El suscriptor puede llamar al siguiente con un valor tantas veces como sea necesario, y complete o error al final. Después de llamar a complete o error , el observable no impulsará ningún valor en la secuencia.

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

El ejemplo anterior imprimirá Value is hi después de 1000 milisegundos.

Crear un observable manualmente cada vez puede volverse detallado y tedioso. Por lo tanto, RxJS tiene muchas funciones para crear un observable. Algunos de los más utilizados son of , from y ajax .

de

of toma una secuencia de valores y la convierte en un flujo:

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

desde

from convierte casi cualquier cosa en un flujo de valores:

 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 toma una URL de cadena o crea un observable que realiza una solicitud HTTP. ajax tiene una función ajax.getJSON , que devuelve solo el objeto de respuesta anidado de la llamada AJAX sin ninguna otra propiedad devuelta por 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) // {...}

Hay muchas más formas de hacer un observable (puedes ver la lista completa aquí).

Operadores

Los operadores son una verdadera potencia de RxJS, que tiene un operador para casi cualquier cosa que necesite. Desde RxJS 6, los operadores no son métodos en el objeto observable sino funciones puras aplicadas en el observable usando un método .pipe .

mapa

map toma una función de argumento único y aplica una proyección en cada elemento de la secuencia:

 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 

Mapa

filtrar

El filter toma un solo argumento y elimina los valores de la secuencia que devuelven falso para la función dada:

 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 

Filtrar

mapa plano

El operador flatMap toma una función que asigna cada elemento en el vapor a otro flujo y aplana todos los valores de estos flujos:

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

Mapa plano

unir

merge fusiona elementos de dos flujos en el orden en que llegan:

 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 

Unir

Una lista completa de operadores está disponible aquí.

Redux-Observable

Redux-Observable

Por diseño, todas las acciones en Redux son sincrónicas. Redux-observable es un middleware para Redux que utiliza secuencias observables para realizar un trabajo asíncrono y luego envía otra acción en Redux con el resultado de ese trabajo asíncrono.

Redux-observable se basa en la idea de Epics . Una épica es una función que toma un flujo de acciones y, opcionalmente, un flujo de estado y devuelve un flujo de acciones.

función (acción$: Observable , estado$: EstadoObservable ): observable ;

Por convención, cada variable que es una secuencia (_aka _observable ) termina con $ . Antes de que podamos usar redux-observable, debemos agregarlo como un middleware en nuestra tienda. Dado que las epopeyas son flujos de observables y cada acción que sale de este vapor se canaliza de regreso al flujo, devolver la misma acción dará como resultado un bucle 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' }) )

Piense en esta arquitectura reactiva como un sistema de tuberías donde la salida de cada tubería retroalimenta a cada tubería, incluyéndose a sí misma, y ​​también a los reductores de Redux. Son los filtros en la parte superior de estas tuberías los que deciden qué entra y qué se bloquea.

Veamos cómo funcionaría una epopeya de Ping-Pong. Toma un ping, lo envía al servidor y, una vez que se completa la solicitud, envía un ping de regreso a la aplicación.

 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: las epopeyas son como cualquier otro flujo observable en RxJS. Pueden terminar en un estado completo o de error. Después de este estado, la epopeya y su aplicación dejarán de funcionar. Por lo tanto, debe detectar todos los errores potenciales en el vapor. Puede usar el operador __catchError__ para esto. Más información: Manejo de errores en redux-observable .

Una aplicación reactiva Todo

Con algo de interfaz de usuario agregada, una aplicación de demostración (mínima) se parece a esto:

Una aplicación reactiva Todo
El código fuente de esta aplicación está disponible en Github. Pruebe el proyecto en Expo o escanee el código QR de arriba en la aplicación Expo.

Un tutorial de React, Redux y RxJS resumido

Aprendimos qué son las aplicaciones reactivas. También aprendimos sobre Redux, RxJS y redux-observable, e incluso creamos una aplicación Todo reactiva en Expo con React Native. Para los desarrolladores de React y React Native, las tendencias actuales ofrecen algunas opciones de administración de estado muy poderosas.

Una vez más, el código fuente de esta aplicación está en GitHub. Siéntase libre de compartir sus pensamientos sobre la administración de estado para aplicaciones reactivas en los comentarios a continuación.