发布于 2026-01-06 3 阅读
0

使用 React 的 RxJS

使用 React 的 RxJS

RxJS 和 React 就像巧克力和花生酱一样:单独使用都很棒,但结合起来就变得非常出色。

两个人不小心撞到了一起,把巧克力和花生酱混在了一起,但之后似乎还挺喜欢这种混合物。

在 npm 上快速搜索一下,你会发现很多用于将 RxJS Observables 连接到 React 组件的钩子,但我们还是从头开始吧,因为 RxJS 和 React “本身”就非常契合,因为它们遵循相同的理念,并且拥有非常兼容的 API。

顺便提一下为什么需要 RxJS

2019 年是 RxJS 元年,rxjs.liveng-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>
  );
}
Enter fullscreen mode Exit fullscreen mode

(请在 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);
Enter fullscreen mode Exit fullscreen mode

供参考: 我将遵循给可观察变量加上 $ 后缀的命名惯例(又称芬兰式命名法),这完全是可选的,但我认为这可能有助于学习时的清晰度。

现在我们想要做的是将组件状态与 Observable 中的状态同步。这可以被视为React 函数组件的副作用App,因此我们将使用useEffect()hook,我们可以从 . 导入它react

在回调函数内部,useEffect()我们将:

  • names$使用该方法订阅Observable subscribe(),并将我们的“状态设置函数”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>
  );
}
Enter fullscreen mode Exit fullscreen mode

此时它应该看起来像这样:

RxJS 和 React 的概念和 API 非常兼容:它们的运作方式useEffect与 RxJS 订阅机制一致,而且清理调用正是取消订阅的绝佳时机。随着学习的深入,你会看到更多这种“共生”现象。

题外话useEffect

当使用useEffect同步组件状态与某个“外部”状态同步时,必须决定要与哪个状态同步。

  • 所有州
  • 无州
  • 一些特定的州

这体现在deps数组中,数组是传递给函数的第二个参数useEffect

引用瑞安·弗洛伦斯的一句话:

useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])

所以,在这种情况下,我们没有任何 props 或其他状态需要同步:我们只想让 names 数组的值等于 Observable 的当前值。我们只想在 Observable 的值改变时更新组件状态,因此我们将选择“无状态”,并将一个空数组[]作为第二个参数传入。

useEffect(() => {
  const subscription = names$.subscribe(setNames);
  return () => subscription.unsubscribe();
}, []);
Enter fullscreen mode Exit fullscreen mode

创建自定义钩子

看来我们会经常用到这种模式:

  • 订阅 ObservableuseEffect
  • 设置任何更改时的状态
  • 在清理函数中取消订阅

所以,让我们把这种行为提取到一个名为的自定义钩子useObservable中。

const useObservable = observable => {
  const [state, setState] = useState();

  useEffect(() => {
    const sub = observable.subscribe(setState);
    return () => sub.unsubscribe();
  }, [observable]);

  return state;
};
Enter fullscreen mode Exit fullscreen mode

我们的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)));
Enter fullscreen mode Exit fullscreen mode

不错。这样我们就能看到列表逐一出现了。虽然有点鸡肋,但总算是个好的开始。😄

正在获取一些数据

我们不使用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}`;
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

这将从 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