使用 React 的 RxJS
RxJS 和 React 就像巧克力和花生酱一样:单独使用都很棒,但结合起来就变得非常出色。
在 npm 上快速搜索一下,你会发现很多用于将 RxJS Observables 连接到 React 组件的钩子,但我们还是从头开始吧,因为 RxJS 和 React “本身”就非常契合,因为它们遵循相同的理念,并且拥有非常兼容的 API。
顺便提一下为什么需要 RxJS
2019 年是 RxJS 元年,rxjs.live和ng-conf等活动让 RxJS 在 Web 开发社区迅速走红。越来越多的开发者发现 RxJS 非常强大,即使学习曲线略显陡峭,也绝对值得投入时间精力。
Angular 开发者使用 RxJS 已经有一段时间了。只需简单搜索一下,你就会发现 vue-rx、ember-rx,甚至 Svelte 都默认使用 RxJS Observables 作为 store。学习 RxJS 意味着掌握一项高度可移植的技能,它可以在各种框架中使用。事实上,Rx 的概念可以跨语言和平台应用。
RxJS 是一个成熟且久经考验的事件和数据流处理库。熟悉它的工作原理绝对会对你大有裨益。
我们先来看一个简单的例子:
这里我们有一个简单的List组件,它只是列出给定的字符串:
const source = ['Adam', 'Brian', 'Christine'];
function App() {
const [names, setNames] = useState(source);
return (
<div className="App">
<h1>RxJS with React</h1>
<List items={names} />
</div>
);
}
(请在 CodeSandbox 上跟着操作!)
现在,让我们从 RxJS Observable 中提取这些值。
我们先从使用 RxJSof()函数创建一个 Observable 开始。
我们需要:
- 添加
rxjs为依赖项(npm i rxjs或者yarn add rxjs如果您未使用 CodeSandbox,则需要使用其他方式) - 从
ofrxjs
然后我们创建一个名为 `<Observable_name>` 的 Observable names$,其值为source数组:
import { of } from 'rxjs';
const source = ['Adam', 'Brian', 'Christine'];
const names$ = of(source);
供参考: 我将遵循给可观察变量加上 $ 后缀的命名惯例(又称芬兰式命名法),这完全是可选的,但我认为这可能有助于学习时的清晰度。
现在我们想要做的是将组件状态与 Observable 中的状态同步。这可以被视为React 函数组件的副作用App,因此我们将使用useEffect()hook,我们可以从 . 导入它react。
在回调函数内部,useEffect()我们将:
names$使用该方法订阅Observablesubscribe(),并将我们的“状态设置函数”setNames作为观察者参数传递。- 捕获
subscription返回值observable.subscribe() .unsubscribe()返回一个调用订阅方法的清理函数
function App() {
const [names, setNames] = useState();
useEffect(() => {
const subscription = names$.subscribe(setNames);
return () => subscription.unsubscribe();
});
return (
<div className="App">
<h1>RxJS with React</h1>
<List items={names} />
</div>
);
}
此时它应该看起来像这样:
RxJS 和 React 的概念和 API 非常兼容:它们的运作方式useEffect与 RxJS 订阅机制一致,而且清理调用正是取消订阅的绝佳时机。随着学习的深入,你会看到更多这种“共生”现象。
题外话useEffect
当使用useEffect同步组件状态与某个“外部”状态同步时,必须决定要与哪个状态同步。
- 所有州
- 无州
- 一些特定的州
这体现在deps数组中,数组是传递给函数的第二个参数useEffect。
引用瑞安·弗洛伦斯的一句话:
useEffect(fn) // all stateuseEffect(fn, []) // no stateuseEffect(fn, [these, states])
所以,在这种情况下,我们没有任何 props 或其他状态需要同步:我们只想让 names 数组的值等于 Observable 的当前值。我们只想在 Observable 的值改变时更新组件状态,因此我们将选择“无状态”,并将一个空数组[]作为第二个参数传入。
useEffect(() => {
const subscription = names$.subscribe(setNames);
return () => subscription.unsubscribe();
}, []);
创建自定义钩子
看来我们会经常用到这种模式:
- 订阅 Observable
useEffect - 设置任何更改时的状态
- 在清理函数中取消订阅
所以,让我们把这种行为提取到一个名为的自定义钩子useObservable中。
const useObservable = observable => {
const [state, setState] = useState();
useEffect(() => {
const sub = observable.subscribe(setState);
return () => sub.unsubscribe();
}, [observable]);
return state;
};
我们的useObservable钩子接受一个 Observable 对象,并返回该 Observable 对象发出的最后一个值,同时通过调用来触发更改时的重新渲染setState。
请注意,我们的状态会一直初始化,undefined直到 Observable 中发出某个值。我们稍后会用到它,但现在,请确保组件能够处理 Observable 为空的state情况undefined。
所以我们现在应该得到类似这样的结果:
当然,我们可以(而且可能应该)将它useObservable()定义为模块的导出项,并放在单独的文件中,因为它可以在组件之间共享,甚至可能在应用程序之间共享。但为了今天的简单示例,我们将所有内容都放在一个文件中。
增加一些异步性
现在我们看到了一串名字,但这到目前为止都很无聊,所以我们来做一些更异步的事情吧。
让我们interval从`import`rxjs和 ` mapoperator` 导入rxjs/operators。然后,让我们使用它们创建一个 Observable,该 Observable 每秒只向列表中添加一个名称。
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
const source = ['Adam', 'Brian', 'Christine'];
const names$ = interval(1000).pipe(map(i => source.slice(0, i + 1)));
不错。这样我们就能看到列表逐一出现了。虽然有点鸡肋,但总算是个好的开始。😄
正在获取一些数据
我们不使用source数组,而是从 API 获取名称列表。
我们将使用的 API 端点来自randomuser.me,这是一个不错的服务,可以用来获取一些虚构的用户数据。
我们将添加这两个辅助变量,api以便getName一次获取 5 个用户,并且该函数将帮助从 randomuser.me 提供的用户数据中提取姓名。
const api = `https://randomuser.me/api/?results=5&seed=rx-react&nat=us&inc=name&noinfo`;
const getName = user => `${user.name.first} ${user.name.last}`;
RxJS有一些很棒的实用函数用于获取数据,例如 `getData()`fromFetch和 `getJson webSocket()`,但由于我们只是从 ajax 请求中获取一些 JSON,因此我们将使用 RxJS模块ajax.getJSON中的方法rxjs/ajax。
import { ajax } from 'rxjs/ajax';
const names$ = ajax
.getJSON(api)
.pipe(map(({ results: users }) => users.map(getName)));
这将从 API 获取前 5 个用户,并遍历数组以从每个用户的 `name`name.first和 `name`name.last属性中提取名称。现在我们的组件正在渲染从 API 获取的 5 个名称,太棒了!
值得注意的是,自从我们将代码移到自定义钩子中后,组件代码完全没有改动。像这样将数据与组件的显示解耦,可以带来一些优势。例如,我们可以将 Observable 连接到 WebSocket 以实现实时数据更新,甚至可以使用 Web Worker 进行轮询,但组件本身无需更改,它只需渲染接收到的任何数据即可,而且数据获取方式的实现与页面上的显示完全隔离。
除了 RxJS Ajax 之外
使用 RxJS ajax 模块(以及fromFetch)的一大好处是,请求取消功能是内置的。
由于我们的useObservable钩子会在清理函数中取消订阅 Observable,因此如果组件在 Ajax 请求进行期间被“卸载”,则 Ajax 请求将被取消,并且setState不会再调用该钩子。这是一个无需任何额外操作即可实现的出色内存安全特性。RxJS 和 React 再次完美地协同工作,开箱即用。
行动
现在我们有了这个很棒的自定义钩子,可以从 Observable 中读取状态值。这些值可以来自任何地方,异步地传入我们的组件,这很不错,但 React 的核心在于数据向下传递,动作向上执行(DDAU)。目前我们只实现了数据部分,那么动作部分呢?
在本系列的下一期中,我们将探讨Actions,我们将如何模仿内置的 useReducer hook 来构建 RxJS 集成,以及更多内容。
如果您有任何疑问,欢迎在评论区留言,或者您可以加入我们的 Bitovi 社区 Slack 频道(https://bitovi.com/community/slack),直接向我提问。那里还有很多其他 JavaScript 专家,是提问或寻求帮助的好地方。
文章来源:https://dev.to/bitovi/rxjs-with-react-jek

