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:

  1. Where is the UI rendering data defined?
  2. Where are Rxjs streams built?
  3. 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 cyclecreatedTime:
    • The key of the same name, defined as responsive data, hangs on the VM instance, which is herenumWill hang onvm.num;
    • For each OB, hang invm.$observablesOn, that is,vm.$observables.numOb can be obtained, but it doesn’t seem to be useful… ;
    • Perform ob, data subscription, and assign the same namevm[key], i.e.,vm.numIt is bound to ob (which is a vm Subscription object).
  • Through mixins, in the life cyclebeforeDestroyWhen: 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:

  1. Where to make the final consumption data definition, prepare a pit;
  2. Stream logic: build stream, what stream is => stream execution => data subscription, data assignment;
  3. 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!