0%

【英文阅读】React Hooks vs RxJS

I’m currently working a lot with React which is a nice change of scenery. Coming from Angular I’ve had to learn quite a frew things about the framework while I was able to re-use basic web development skills (HTML, (S)CSS, JavaScript/TypeScript) and transfer concepts like component-orientation.

我目前正在大量使用React,这是一个很好的场景切换。由于Angular背景,我不得不学习很多关于框架的东西,同时我能够重用基本的Web开发技能(HTML、(S)CSS、JavaScript、TypeScript),并转换一些概念,比如组件化。

Glancing at React hooks I also hoped to profit off of my experience with reactive programming - but that didn’t realy turn out to be the case and here’s why.

浏览了一下React钩子,我还希望从我在响应式编程的经验中获益——但事实并非如此,下面会解释为什么会这样。

Using Angular made me learn RxJS and its underlying concept of observable.

使用Angular让我学习了RxJS和它的Observable的基本概念。

The nice thing here is that RxJS and reactive programming in general is fundamentally decoupled from any framework - it’s a generic paradigm that you can apply in all sorts of domains where you’re dealing with asynchronous problems.

这里的好处是,RxJS和响应式编程通常从根本上与任何框架解耦——它是一种通用范式,你可以将其应用于处理异步问题的各种领域。

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections. – RxJS Docs

In fact, RxJS is one implementation of the ReactiveX API which is also available for numerous other languages.

事实上,RxJS是ReactiveX API的一种实现,它也可以用于许多其他语言。

However, ideas from reactive programming aren’t limited to that project but can now be found in many places and practically any modern user-interface framework*.

然而,响应式编程的想法不仅仅限于该项目,现在可以在许多地方以及几乎任何现代用户界面框架中找到:

  • Vue.js has an observable implementation use for change detection and state management.
  • Angular leverages RxJS observables from component communication, HTTP requests and providing event listeners on forms or the router.
  • React, well, it has it in the name, ain’t it?

Actually, kind of yes and also kind of no for that last one. The name definitely relates to reactivity in the sense that React reacts to changes of a component’s state by scheduling an update of the corresponding view.

实际上,对于最后一个,有点是,也有点不是。这个名字肯定与响应式有关,因为React通过调度相应视图的更新来对组件状态的变化作出反应。

Apart from that, the framework is explicitly not concerned with reactive programming.

除此之外,该框架明确不关心响应式编程。

There is an internal joke in the team that React should have been called “Schedule” because React does not want to be fully “reactive”.

– React Docs

It might seem like that has changed with the introducation of function components and specifically hooks.

随着函数式组件和特别是hooks的引入,这似乎已经发生了变化。

Let’s see how they compare to RxJS observables by looking at an example where we encapsulate an HTTP request made with the Fetch API.

让我们通过一个示例来看看它们与RxJS可观察对象的比较,我们封装了一个使用Fetch API发出的HTTP请求。

We’ll start with the RxJS version where we can create a custom observable with the Observable constructor. It accepts a callback (or subscribe function) which will be executed upon subscription.

我们将从RxJS版本开始,我们可以使用Observable构造函数创建自定义的observable。它接受将在订阅时执行的回调(或订阅函数)。

This callback receives a subscriber to whom we can subsequently emit values via subscribe.next() as well as signalize completion or failure of the underlying operation via subscriber.complete() and subscriber.error() respectively.

这个回调接收一个subscriber,我们随后可以通过subscriber.next()向其发送值,并分别通过subscriber.complete()subscriber.error()发出底层操作完成或失败的信号。

A subscribe function can optionally return other callback (teardown logic) that’ll be invoked when the observable is unsubscribed. This allow us to implement cancellation.

订阅函数可以选择返回另一个回调(取消逻辑),当observable取消订阅时将调用该回调。这使我们能够实现取消逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {Observable} from "rxjs";

export function Fetch() {
const getUsers = () => {
return new Observable(subscriber => {
const controller = new AbortController()
const {signal} = controller;
fetch('https://www.baidu.com', {signal})
.then(response => response.json)
.then(data => {
subscriber.next(data);
subscriber.complete();
})
.catch(err => subscriber.error(err))
return () => controller.abort();
})
}
}

In our comcrete case, we’ll first use the Promise-based Fetch API to executed and parse the HTTP request before emitting the response to the subscriber and immediately completing the observable.

在我们具体的案例中,我们将首先使用基于Promise的Fetch API来执行和解析HTTP请求,然后将响应发送给订阅者并立即完成observable。

When the request should fail, the subscriber will also be notified. We’ll additionally have teardown logic levering an AbortController to cancel the HTTP request.

当请求失败时,也会通知订阅者。此外我们还将使用AbortController来取消HTTP请求的销毁逻辑。

Here’s how we could use our little RxJS HTTP client:

下面展示了我们如何使用我们的小型RxJS HTTP客户端:

1
2
3
4
5
6
7
8
9
10
11
const subscription = getUsers().subscribe(
data => {
console.log('success, data: ', data)
},
error => {
console.log('error', error)
},
() => {
console.log('request finished')
}
)

Now, let’s sess how we’d implement a similar HTTP client within a customer React hook. First off, we’d probably rename the encapsulating function so that it contains the “use” prefix to comply with the rules of hooks.

现在,让我们看看如何在自定义React钩子中实现类似的HTTP客户端。首先,我们可能会重命名封装函数,使其包含“use”前缀以符合钩子规则。

Then we create a functional two state variables with useState for holding a the response from our HTTP request and possibly an error. Our hook will always return the most recent states of both variables as tuple-both starting off with as undefined.

然后我们使用useState创建功能性的两个状态变量,用于保存来自我们HTTP请求的响应以及可能的错误。我们的钩子总是将两个变量的最新状态作为一个元祖返回——两者都以undefined开始。

The next build-in hook that we’ll leverage is useEffect which can be used similar to the Observable constructor: pass a callback where we tick off an HTTP request, pass the response and update the state - we can even return a teardown function.

我们利用的下一个内置钩子是useEffect,它可以类似于Observable构造函数使用:传递一个回调函数,在回调函数中我们启动一个HTTP请求,解析响应结果并更新状态——我们甚至可以返回一个取消函数。

The second parameter is an optional list of values that will be watched by React. When one of the values changes, the effect will be run again. Passing an empty list makes sure that our effect runs only once when a component which uses the hook is created. In turn, when you pass no list at all the effect will run every time the component is re-rendered.

第二个参数是可选的值列表,React将会监控它。当其中的一个值发生变化,effect函数将再次运行。传递一个空列表确保我们的effect函数只在组件创建的时候运行一次,反过来,如果根本不传递任何列表时,每次组件重新渲染的时候它都会运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const useUsers = () => {
const [users, setUsers] = useState()
const [error, setError] = useState()
useEffect(() => {
const controller = new AbortController();
const {signal} = controller;
fetch('api/images/list', signal)
.then(response => response.json())
.then(res=> setUsers(res.data))
.catch(err => setError(err));
return () => controller.abort();
}, [])
return [error, users]
}

And here’s how we’d use our custom hook inside a React component:

这里展示我们如何在React组件中使用这个自定义的钩子:

1
2
3
4
5
6
7
8
9
10
const User = () => {
const [error, users] = useUsers();
if (!users) {
return <p>starting request</p>
}
if (error) {
return <p>request error: {error.message}</p>
}
return <p>received: {users.count}</p>
}

Looks all very similar, doesn’t it? Coming from Angular you’d think the latter is just the React way of doing reactive programming - but it’s not. It’s the React way of scheduling view updates and we’re leveraging it to encapsulate asynchonous logic.

看起来都非常相似,不是吗?来自于Angular的思维让你认为后者只是React方式的响应式编程——但事实并非如此。这只是React调度视图更新的方式,我们只是利用它来封装异步逻辑。

I wonder how many people realize that React Hooks is really just disconnected, less declarative reactive programming.

– Ben Lesh, RxJS Team Lead.

Ben has made some great points comparing React hooks and observables here.

Ben 在这里(推特)上提出了一些比较React Hooks和Observables的重要的点。

The point that I want to get across the most: hooks are React, reactivity is universal.

我最想表达的观点是:hooks是react的,响应式是大众的。

You can easily get this from the fact that getUsers() from our example can be used with and without Angular while useUsers() only make sense when used inside a React component. Eventually, the “reactivity” of React hooks is opaque and hard-wired to the framework.

你可以很容易的从我们示例中得到这个事实:getUsers()可以在有或者没有Angular的情况下使用,而useUsers()只有在React组件中使用才有意义。最终,React Hooks的“响应式”是不透明而且硬链接到框架的。

It’s not evident from the type of a hooked variable that it may change (e.g. number vs Observable<number>). Hooks also don’t realy have an API surface. Instead they rely on the way you order you calls and how React schedules view updates.

从钩子变量的类型很难看出来它会发生改变(就像numberObservable<number>相比)。Hooks也没有真正的API展示。相反,他们只依赖于你提供的调用以及React怎样安排视图更新。

Those are also the reason why you should do things like prefixing custom hooks with “use” - whether that rolls off the tongue or not.

这也是为什么你应该在自定义钩子前面加上“use”的原因——不管它是否拗口。

Hooks are fascinating piece of work that highlights the power of functional programming, specifically closures. I’d recommend you read the well-written article Deep dive: How do React hooks really work? by swyx to seee this for yourself.

Hooks在工作中是一项引人入胜的功能,它突出了函数式编程的力量,特别是闭包。我建议你阅读一篇好文章:React hooks 是如何工作的,可以去推特swyx亲自查看。

The thing to keep in mind is that hooks are first and foremost focused on component rendering. They’re not primarily meant for composing asynchonous, possibly long-living event streams - also known as reactive programming.

需要记住的是,Hooks首先关注的是组件渲染。他们主要不是用于编写异步、可能是长期存在的事件流——也称之为响应式编程。

Fortunately, RxJS and React hooks don’t exclude each other. We can make them get along via a custom hook that takes an observables, subscribes to it and forwards events into hooked state variables:

幸运的是,RxJS和React Hooks并不互相排斥。我们可以通过一个自定义钩子让它们结合起来,该钩子接受一个可观察对象,订阅它并将事件转发到钩子的状态变量中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {Observable} from "rxjs";
import {useEffect, useState} from "react";

export const useObservable = (observable) => {
const [value, setValue] = useState()
const [error, setError] = useState()

useEffect(() => {
const subscription = observable.subscribe(setValue, setError)
return () => subscription.unsubscribe();
}, [])
return [error, value]
}

export const getUsers = () => {
return new Observable(subscriber => {
const controller = new AbortController();
const {signal} = controller
fetch('/api/images/list', {signal})
.then(response => response.json())
.then(res=> {
subscriber.next(res.data)
subscriber.complete();
})
.catch(err => subscriber.error(err))
return () => controller.abort();
})
}

Once we have this little reactive helper we can replace our custom useUsers() hook with the existing RxJS-based HTTP client:

我们有了这个响应式助手,我们就可以用现有的基于RxJS的HTTP客户端替换我们自定义useUsers()钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {getUsers, useObservable} from "./useObservable";

export function FetchRxjs() {
const Users = () => {
const [error, users] = useObservable(getUsers());
if (!users) {
return <p>starting request</p>
}
if (error) {
return <p>request error: {error.message}</p>
}
return <p>received: {users.count}</p>
}

return (
<div>
<h2>fetch</h2>
<Users/>
</div>
)
}

Essentially, such a hook is similar to Angular’s AsyncPipe or a similar structural directive. It’s a bridge for synchonizing reactive code with a framework’s change detection mechanism.

本质上,这样的钩子类似于Angular的AsyncPipe或者类似于结构指令。它是使响应式代码与框架的变更检测机制同步的桥梁。

Again, in practice you could use a more battle-tested solution like one of the following:

同样,在实践中,你可以使用更成熟的解决方案,如下所示:

Now, don’t get me wrong, using hooks for asynchonous code is fine in many cases. After all, observables are probably not the most convenient abstraction for asynchonous operations that produce a single value like HTTP requests.

现在,不要误会我的意思,在很多情况下使用钩子处理异步代码是可以的。比较,对于像HTTP请求这样产生单个值的异步操作来讲,observable可能不是最方便的抽象。

I won’t argue that you should outsource all logic via RxJS especiall if it’s not asynchonous. Rather, I want you to understand the trade-off that you’re making when coupling logic to the change detection mechanism of you chosen view framework.

我不会争辩说你应当将所有的逻辑都用RxJS实现,特别是非异步的逻辑。相反,我希望你了解在将逻辑和框架的变更检测机制耦合时所做的权衡。

More importantly, I want to show the strength of Observable as a universal abstraction which will allow you to write portable and framework-independent code for working with asynchonous event collections.

更重要的是,我想展示Observable作为通用抽象的力量,它将允许你编写可移植且独立于框架的代码来处理异步事件集合。


完~

感兴趣可阅读原文博客。

Link:

React Hooks vs RxJS

码字辛苦,打赏个咖啡☕️可好?💘