This article is written by team members, and we have authorized the exclusive use of Doodle Big Front-end, including but not limited to editing, original annotation and other rights.
This article describes how to use Rxjs in React, Vue, and the open-source rxJs-hooks and vue-rx. Before you begin, I hope you have a basic understanding of responsive programming, Rxjs. Let’s get started!
A perfect partnership
Functions of the front-end framework (e.g. React and Vue) : Synchronize data with the UI. When data changes, the UI refreshes automatically.
UI = f(data)
Copy the code
What responsive programming does (like Rxjs) : Focus on the data, from the source of the data stream, to the processing of the data, to the subscription of the data (the consumption of the data);
data = g(source)
Copy the code
The relationship between the two is not in conflict, and in some cases is a perfect partnership, with the front-end framework acting as a consumer of responsive programming data:
UI = f(g(source))
Copy the code
Is it similar to MV definition:
MVVM happens to be a good fit for Rx*. Quoting Wikipedia:
The view model of MVVM is a value converter meaning that the view model is responsible for exposing the data objects from the model in such a way that those objects are easily managed and consumed. In this respect, The view model is more model than the view, and handles most if not all of the view’s display logic.
React: rxjs-hooks
React (considering only functional components) has two forms for expressing “non-one-time assignment” directly:
useMemo
const greeting = React.useMemo(() = > `${greet}.${name}! `, [greet, name]);
Copy the code
useState
+useEffect
const [greeting, setGreeting] = useState(() = > `${greet}.${name}! `);
useEffect(() = > {
setGreeting(() = > `${greet}.${name}! `);
}, [greet, name]);
Copy the code
Note: useMemo computs data before render, useState+useEffect computs data after render.
To access Rxjs, you need to build the whole “pipeline”, including Observable preparation, data processing, data subscription, and even some side effects (TAP), which is beyond the capacity of useMemo. UseEffect, on the other hand, is a good fit (for side effects) to try extending with useState+useEffect.
Let’s start with a basic version:
import * as React from 'react';
import { combineLatest, from.of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const GreetSomeone = ({ greet = 'Hello' }) = > {
const [greeting, setGreeting] = React.useState(' ');
React.useEffect(() = > {
const greet$ = of(greet);
// fetchSomeName: Searches data remotely
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() = > of('Mololongo')));const greeting$ = combineLatest(greet$, name$).pipe(
map(([greet, name]) = > `${greet}.${name}! `));const subscription = greeting$.subscribe(value= > {
setGreeting(value);
});
return () = >{ subscription.unsubscribe(); }} []);return <p>{greeting}</p>;
};
Copy the code
Rxjs stream is set up in useEffect. Once the data is subscribed, the data is recorded in the component for data rendering and unsubscribed when the component is destroyed.
There is a problem, however, that the prop greet received by the component will change and the data of greet$will not be updated. How do you solve it? If handled like this:
React.useEffect(() = > {
const greet$ = of(greet);
/** * as above, the flow builds logic **/
}, [greet]);
Copy the code
The problem is that every time the Rxjs stream is regenerated by greet, the fetchSomeName call will be called again. The cost is a bit high.
How do you solve it?
Introduce a useEffect that actively pushes data with Rxjs subject.next, while ensuring that the Rxjs stream is built only once, with the complete code attached:
import * as React from 'react';
import { BehaviorSubject, combineLatest, from.of } from 'rxjs';
import { catchError, map, startWith } from 'rxjs/operators';
const GreetSomeone = ({ greet = 'Hello' }) = > {
// Use react. useRef to remain unchanged during the component lifecycle
const greet$ = React.useRef(new BehaviorSubject(greet));
// subject. next pushes data to make Rxjs data updated
React.useEffect(() = > {
greet$.current.next(greet);
}, [greet]);
const [greeting, setGreeting] = React.useState(' ');
// The logic remains the same
React.useEffect(() = > {
const name$ = from(fetchSomeName()).pipe(
startWith('World'),
catchError(() = > of('Mololongo')));const greeting$ = combineLatest(greet$.current, name$).pipe(
map(([greet, name]) = > `${greet}.${name}! `));const subscription = greeting$.subscribe(value= > {
setGreeting(value);
});
return () = > {
subscription.unsubscribe();
}
}, [greet$]);
return <p>{greeting}</p>;
};
Copy the code
With a basic understanding of the hooks implementation, let’s take a look at the open source implementation of rxjs-hooks, which describes itself very simply:
React hooks for RxJS.
Rxjs-hooks Design two hooks, useObservable and useEventCallback.
See useObservable: after removing the TS type, see if it is consistent with the above structure
export function useObservable(inputFactory, initialState, inputs,){
const [state, setState] = useState(typeofinitialState ! = ='undefined' ? initialState : null)
const state$ = useConstant(() = > new BehaviorSubject(initialState))
const inputs$ = useConstant(() = > new BehaviorSubject(inputs))
useEffect(() = > {
inputs$.next(inputs)
}, inputs || [])
useEffect(() = > {
let output$
if (inputs) {
output$ = inputFactory(state$, inputs$)
} else {
output$ = inputFactory(state$)
}
const subscription = output$.subscribe((value) = > {
state$.next(value)
setState(value)
})
return () = > {
subscription.unsubscribe()
inputs$.complete()
state$.complete()
}
}, []) // immutable forever
return state
}
Copy the code
Examples:
import React from 'react'
import ReactDOM from 'react-dom'
import { useObservable } from 'rxjs-hooks'
import { of } from 'rxjs'
import { map } from 'rxjs/operators'
function App(props: { foo: number }) {
const value = useObservable((_, inputs$) = > inputs$.pipe(
map(([val]) = > val + 1),),200, [props.foo])
return (
// render three times
// 200 and 1001 and 2001
<h1>{value}</h1>)}Copy the code
UseObservable builds an Observable for props and state, and returns the subscribed data. So the inputs are: inputFactory (the build logic for the Rxjs stream), initialState, inputs.
UseEventCallback Similarly, a hook returns the subscribed data, and a callback handles the event response:
const event$ = useConstant(() = > new Subject<EventValue>())
function eventCallback(e: EventValue) {
return event$.next(e)
}
return [returnedCallback as VoidableEventCallback<EventValue>, state]
Copy the code
Consider: RXJS landing environment needs conditions
There are three problems to be solved:
- Where is the UI rendering data defined?
- Where are Rxjs streams built?
- How does Rxjs stream makeObservableContinuously emit value and flow?
Move hands: Vue + Rxjs
With the same idea, try using Rxjs in Vue:
<template>
<div>{{ greeting }}</div>
</template>
<script>
import { from, combineLatest, BehaviorSubject } from "rxjs";
import { map } from "rxjs/operators";
let subscription = null,
greet$ = null;
export default {
name: "TryRxInVue".props: {
greet: {
type: String.default: "hello",}},data() {
return {
greeting: ""}; },// Listen on dependencies to make flow
watch: {
greet(value) {
this.greet$.next(value); }},// Different lifecycle hooks
mounted() {
this.initStream();
},
beforeDestroy() {
subscription = null;
greet$ = null;
},
methods: {
// Initialize the flow when the component mounts
initStream() {
greet$ = new BehaviorSubject(this.greet);
const name$ = from(Promise.resolve("world"));
const greeting$ = combineLatest(greet$, name$).pipe(
map(([greet, name]) = > `${greet}.${name}! `)); subscription = greeting$.subscribe((value) = > {
this.greeting = value; }); ,}}};</script>
Copy the code
The downside is that the logic is very fragmented, so is there a good encapsulation?
Plugin mechanism provided by Vue!
To summarize: write the build of the flow in the agreed configuration location, translate the configuration through plug-ins, plug in the appropriate lifecycle, listener, and so on.
Compare the implementation of open source libraries
Vue Vue Vue Vue Vue Vue Vue Vue Vue Just like Vue-Router, Vuex, etc., it is a VUE plug-in.
After reading the source code, the train of thought is consistent with their own consideration. Here are some important points to note.
The most core subscriptions configuration, which is used like this:
<template>
<div>
<p>{{ num }}</p>
</div>
</template>
<script>
import { interval } from "rxjs";
export default {
name: "Demo".subscriptions() {
return {
num: interval(1000).pipe(take(10))}; }};</script>
Copy the code
What does it do behind the scenes? How is it translated?
- Through mixins, in the life cycle
created
Time:- The key of the same name, defined as responsive data, hangs on the VM instance, which is here
num
Will hang onvm.num
; - For each OB, hang in
vm.$observables
On, that is,vm.$observables.num
Ob can be obtained, but it doesn’t seem to be useful… ; - Perform ob, data subscription, and assign the same name
vm[key]
, i.e.,vm.num
It is bound to ob (which is a vm Subscription object).
- The key of the same name, defined as responsive data, hangs on the VM instance, which is here
- Through mixins, in the life cycle
beforeDestroy
When: Unsubscribe;
Take a quick look at the source code:
import { defineReactive } from './util'
import { Subject, Subscription } from 'rxjs'
export default {
created () {
const vm = this
/ / subscriptions to come
let obs = vm.$options.subscriptions
if (obs) {
vm.$observables = {}
vm._subscription = new Subscription()
Object.keys(obs).forEach(key= > {
// Reactive data is defined, and the key hangs on the VM instance
defineReactive(vm, key, undefined)
// Obs is also attached to vm.$observables
const ob = vm.$observables[key] = obs[key]
// Perform ob, data subscription, and finally assign to the prepared OBS [key] pit
vm._subscription.add(obs[key].subscribe(value= > {
vm[key] = value
}, (error) = > { throw error }))
})
}
},
beforeDestroy () {
// Unsubscribe
if (this._subscription) {
this._subscription.unsubscribe()
}
}
}
Copy the code
After building subscriptions, the core problem was solved. What remained was how to implement dependency and behavior drives.
How do you implement dependent drivers?
Vue-rx exposes a $watchAsObservable method that works like this:
import { pluck, map } from 'rxjs/operators'
const vm = new Vue({
data: {
a: 1
},
subscriptions () {
// declaratively map to another property with Rx operators
return {
aPlusOne: this.$watchAsObservable('a').pipe(
pluck('newValue'),
map(a= > a + 1)}}})Copy the code
The $watchAsObservable parameter is an expression that returns ob, which pops out when the expression value changes. Its source code implementation invades the New Observable({… }) :
import { Observable, Subscription } from 'rxjs'
export default function watchAsObservable (expOrFn, options) {
const vm = this
const obs$ = new Observable(observer= > {
let _unwatch
const watch = () = > {
_unwatch = vm.$watch(expOrFn, (newValue, oldValue) = > {
observer.next({ oldValue: oldValue, newValue: newValue })
}, options)
}
// This is simple
watch()
// Return unsubscribe
return new Subscription(() = > {
_unwatch && _unwatch()
})
})
return obs$
}
Copy the code
This approach is common in VUE-RX. You will find that the logic is the same as the simple Demo you wrote, except that the logic of ob declaration and observed value changes is encapsulated in the plugin.
How do you implement behavior-driven?
The simple Demo I wrote does not include this, but it does nothing more than define a Subject that participates in the construction of the stream and emits values to drive changes in the stream data as the event responds.
Hey, don’t say, this is really the thing behind vue-Rx providing one of the behavior-driven methods, configuring domStreams with the custom directive V-stream +, which doesn’t do expansion here.
The other approach is an example of VUE-RX exposure, observableMethods, which is implemented quite nicely, but I’ll talk about it briefly. For example, it looks like this:
new Vue({
observableMethods: {
submitHandler: 'submitHandler$'
// or with Array shothand: ['submitHandler']}})Copy the code
It will mount two properties during the Mixin Created life cycle, vm.submitHandler$is an OB that is involved in the build of the stream, vm.submitHandler is the data producer of this OB, the exposed interface, and the parameter is the value that ob emerges from. Such a mechanism includes both the ob declaration and the exposure that drives ob.next. The disadvantage is that which is the driving method and which is OB is not intuitive, relying on convention and cognition, which is not clear and definite.
Vue Composition API
Vue’s new Composition API, which is inspired by React Hooks
Like React hooks, the Vue Composition API is designed to solve the problem of logic fragmentation.
There is a new discussion of how to integrate Rxjs based on the Vue Composition API, and the advantage is that the logic is more aggregated for users.
See Vue Composition API and VUE-Rx for details.
conclusion
First of all, the relationship between Rxjs and React/Vue and other front-end frameworks is clarified. The two can be a cooperative relationship in application.
Second, learn how to integrate RXJS with the front-end framework through rxJs-hooks, VUe-rx. It’s a matter of finding the most appropriate mechanism within a given framework, with hooks in React and Vue’s relatively cumbersome plugins. But essentially, integrating Rxjs solves the same problem:
- Where to make the final consumption data definition, prepare a pit;
- Stream logic: build stream, what stream is => stream execution => data subscription, data assignment;
- Better scenario coverage: how to implement dependence-driven, behavior-driven;
Finally, I hope Rxjs can work its magic in your framework for your daily development!