在 React Native 中使用 Redux、RxJS 和 Redux-Observable 构建反应式应用程序
已发表: 2022-03-11在日益丰富且功能强大的 Web 和移动应用程序的生态系统中,需要管理的状态越来越多,例如当前用户、加载的项目列表、加载状态、错误等等。 Redux 是通过将状态保存在全局对象中来解决此问题的一种方法。
Redux 的限制之一是它不支持开箱即用的异步行为。 一个解决方案是redux-observable
,它基于 RxJS,一个强大的 JavaScript 响应式编程库。 RxJS 是 ReactiveX 的一个实现,ReactiveX 是一种源自微软的反应式编程 API。 ReactiveX 结合了反应范式、函数式编程、观察者模式和迭代器模式的一些最强大的特性。
在本教程中,我们将了解 Redux 及其与 React 的用法。 我们还将探索使用 RxJS 的响应式编程,以及它如何让繁琐而复杂的异步工作变得非常简单。
最后,我们将学习 redux-observable,这是一个利用 RxJS 进行异步工作的库,然后将使用 Redux 和 redux-observable 在 React Native 中构建一个应用程序。
还原
正如它在 GitHub 上描述的那样,Redux 是“JavaScript 应用程序的可预测状态容器”。 它为您的 JavaScript 应用程序提供全局状态,使状态和操作远离 React 组件。
在没有 Redux 的典型 React 应用程序中,我们必须通过属性或props
将数据从根节点传递给子节点。 这种数据流对于小型应用程序是可管理的,但随着应用程序的增长会变得非常复杂。 Redux 允许我们拥有彼此独立的组件,因此我们可以将其用作单一事实来源。
Redux 可以使用react-redux
在 React 中使用,它为 React 组件提供绑定以从 Redux 读取数据并调度操作以更新 Redux 状态。
Redux 可以描述为三个简单的原则:
1. 单一真理来源
整个应用程序的状态存储在单个对象中。 Redux 中的这个对象由 store 持有。 任何 Redux 应用程序中都应该有一个商店。
» console.log(store.getState()) « { user: {...}, todos: {...} }
要在 React 组件中从 Redux 读取数据,我们使用react-redux
的connect
函数。 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 />
的 props 合并。
2. 状态是只读的
Redux 状态对于 React 组件是只读的,改变状态的唯一方法是发出一个动作。 动作是一个简单的对象,表示改变状态的意图。 每个动作对象都必须有一个type
字段,并且值必须是一个字符串。 除此之外,动作的内容完全取决于您,但大多数应用程序都遵循通量标准动作格式,这将动作的结构限制为只有四个键:
-
type
操作的任何字符串标识符。 每个动作都必须有一个独特的动作。 -
payload
任何操作的可选数据。 它可以是任何时间并包含有关操作的信息。 -
error
如果操作表示错误,则任何可选的布尔属性都设置为 true。 这类似于被拒绝的Promise. string
操作的Promise. string
标识符。 每个动作都必须有一个独特的动作。 按照惯例,当error
为true
时,payload
应该是一个错误对象。 -
meta
Meta 可以是任何类型的值。 它适用于不属于有效负载的任何额外信息。
以下是两个动作示例:
store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });
3. 用纯函数改变状态
全局 Redux 状态是使用称为 reducer 的纯函数更改的。 reducer 接受前一个状态和动作并返回下一个状态。 reducer 创建一个新的状态对象,而不是改变现有的状态对象。 根据应用程序的大小,一个 Redux 商店可以有一个 reducer 或多个 reducer。
/* 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 是一个用于 JavaScript 反应式编程的库,它有一个observables的概念,它是观察者可以订阅的数据流,并且这个观察者随着时间的推移传递数据。
observable 的观察者是一个具有三个功能的对象: 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 的对象来创建一个新的observable
,并传入一个接收订阅者(也称为观察者)的函数。 订阅者有三种方法: next
、 error
和complete
。 订阅者可以根据需要多次调用 next ,并最终complete
或error
。 在调用complete
或error
之后,observable 不会将任何值推送到流中。
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) )
上面的示例将在 1000 毫秒后打印Value is hi
。
每次手动创建 observable 会变得冗长乏味。 因此,RxJS 有很多函数可以创建 observable。 一些最常用的是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 请求的 observable。 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) // {...}
还有更多方法可以制作可观察的(您可以在此处查看完整列表)。
运营商
Operators 是 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-Observable
按照设计,Redux 中的所有操作都是同步的。 Redux-observable 是 Redux 的一个中间件,它使用 observable 流来执行异步工作,然后在 Redux 中用异步工作的结果调度另一个动作。
Redux-observable 是基于Epics的思想。 史诗是一个函数,它接受一个动作流,也可以选择一个状态流并返回一个动作流。
函数 (action$: Observable
按照惯例,每个作为流的变量(_aka _observable
)都以$
结尾。 在我们可以使用 redux-observable 之前,我们必须将它作为中间件添加到我们的 store 中。 由于史诗是可观察的流,并且退出此流的每个动作都通过管道返回到流中,因此返回相同的动作将导致无限循环。
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 的 reducer。 正是这些管道顶部的过滤器决定了什么进入,什么被阻塞。
让我们看看乒乓史诗是如何运作的。 它需要一个 ping,将其发送到服务器——并在请求完成后——将一个 pong 发送回应用程序。
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);
_Important:Epics 就像 RxJS 中的任何其他可观察流一样。 它们最终可能处于完整或错误状态。 在此状态之后,史诗和您的应用程序将停止工作。 因此,您必须抓住蒸汽中的每一个潜在错误。 您可以为此使用__catchError__
运算符。 更多信息: redux-observable 中的错误处理。
反应式待办事项应用程序
添加一些 UI 后,一个(最小的)演示应用程序看起来像这样:
React、Redux 和 RxJS 教程总结
我们了解了什么是响应式应用程序。 我们还学习了 Redux、RxJS 和 redux-observable,甚至使用 React Native 在 Expo 中创建了一个响应式 Todo 应用程序。 对于 React 和 React Native 开发人员来说,当前的趋势提供了一些非常强大的状态管理选项。
再一次,这个应用程序的源代码在 GitHub 上。 欢迎在下面的评论中分享您对反应式应用程序状态管理的看法。