📜 ⬆️ ⬇️

Simplify ReactJS Components with RxJs

Introduction


Most likely, many people, having tried these 2 libraries sufficiently, thought about how to use them together productively. RxJs in itself does not shine with simplicity - a lot of functions, definitely, repel beginners. However, having studied and accepted it, we get a very flexible tool for working with asynchronous code.

I mean that by reading this post, you know ReactJS well, and at least represent the essence of RxJs. I will not use Redux in the examples, but everything that will be written below is perfectly projected onto the React + Redux bundle.

Motivation


We have a component that should produce some asynchronous / heavy actions (let's call them “recalculation”) over its props and display the result of their execution. In general, we have 3 types of props :

  1. Parameters for changing which we need to recalculate and render
  2. Parameters for changing which we must use the value of the previous recalculation and render
  3. Parameters which change does not require any recalculation or rendering, however, they will affect the next recalculation

It is very important that we do not make unnecessary movements and recalculate and render only when necessary. For example, consider a component that, using the passed parameter, counts and displays the Fibonacci number. It has the following input:
')
  1. className - css class that should be hung on the root element (type 2)
  2. value - the number according to which is used for calculations (1st type)
  3. useServerCall - the parameter that allows you to calculate by means of a request to the server, or locally (type 3)

Component example
 import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; //,     Promise import calculateFibonacciExternal from './calculateFibonacci'; export default class Fibonacci extends React.Component { //  ,   static propTypes = { className: PropTypes.string, value: PropTypes.number.isRequired, useServerCall: PropTypes.bool.isRequired, }; //  .      //  state = { loading: true, fibonacci: null, }; //    componentWillMount() { //       -   // ,    this.calculateFibonacci(this.props.value, this.props.useServerCall, (fibonacci) => { this.setState({ fibonacci: fibonacci, loading: false, }); }); } //   props componentWillReceiveProps(nextProps) { //  value -   if(nextProps.value !== this.props.value) { this.setState({ loading: true, }); this.calculateFibonacci(nextProps.value, nextProps.useServerCall, (fibonacci) => { this.setState({ fibonacci: fibonacci, loading: false, }); }); } } //    shouldComponentUpdate(nextProps, nextState) { //      ,   useServerCall return this.props.className !== nextProps.className || this.props.value !== nextProps.value || this.state.loading !== nextState.loading || this.state.fibonacci !== nextState.fibonacci; } // ,          //  ,     componentWillUnmount() { this.unmounted = true; } unmounted = false; calculationId = 0; //      ,   //     calculateFibonacci = (value, useServerCall, cb) => { const currentCalculationId = ++this.calculationId; calculateFibonacciExternal(value, useServerCall).then(fibonacci => { if(currentCalculationId === this.calculationId && !this.unmounted) { cb(fibonacci); } }); }; //    render() { return ( <div className={ classnames(this.props.className, this.state.loading && 'loading') }> { this.state.loading ? 'Loading...' : `Fibonacci of ${this.props.value} = ${this.state.fibonacci}` } </div> ); } } 


It turned out as difficult: the whole code is spread over the 4th methods of the component life cycle, where it looks related, comparing with the previous states, it is easy to forget or break something when updating. Let's try to make this code better.

Introducing react-rx-props


I wrote this small library in order to make the solution of this issue in a more concise way. It consists of two higher order components (HoC, Higher Order Component):

  1. reactRxProps - converts incoming props (with some exceptions) into Observables and sends them to your component
  2. reactRxPropsConnect - takes the logic of working with Observables from your component, allowing you to make it without an internal state (stateless)

Using the first HoC, we get:

Sample component using reactRxProps
 import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { reactRxProps } from 'react-rx-props'; import { Observable } from 'rxjs'; import calculateFibonacciExternal from './calculateFibonacci'; //  Promise  Observable  . const calculateFibonacci = (...args) => Observable.fromPromise(calculateFibonacciExternal(...args)); class FibonacciReactRxProps extends React.Component { // ,     Observables //$        ( ) static propTypes = { className: PropTypes.string, value$: PropTypes.instanceOf(Observable).isRequired, useServerCall$: PropTypes.instanceOf(Observable).isRequired, exist$: PropTypes.instanceOf(Observable).isRequired, }; //       state = { loading: true, fibonacci: null, }; //            componentWillMount() { //useServerCall   ,       this.props.useServerCall$.subscribe(useServerCall => { this.useServerCall = useServerCall; }); //value         this.props.value$.switchMap(value => { this.value = value; this.setState({ loading: true, }); return calculateFibonacci(value, this.useServerCall) .takeUntil(this.props.exist$); //       }).subscribe(fibonacci => { this.setState({ loading: false, fibonacci: fibonacci, }); }); //     className,     propTypes -    //Observable.       . //  props,      Observables } //    render() { return ( <div className={ classnames(this.props.className, this.state.loading && 'loading') }> { this.state.loading ? 'Loading...' : `Fibonacci of ${this.value} = ${this.state.fibonacci}` } </div> ); } } // HoC,     (  ) export default reactRxProps({ propTypes: { className: PropTypes.string, value: PropTypes.number.isRequired, useServerCall: PropTypes.bool.isRequired, }, })(FibonacciReactRxProps); 


What are the advantages compared with the original component:

  1. All the logic of when to do the recalculation and rendering in one place
  2. No duplicate code
  3. No comparisons with the previous state.
  4. We can always automatically unsubscribe from any Observable using takeUntil(this.props.exist$)
  5. All the logic that we do not need no actual results of calculations lies in the launch of switchMap

However, the component still has an internal state, which complicates its testing. Let's use the second hoc:

An example of a component with no internal state
 import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { reactRxProps, reactRxPropsConnect } from 'react-rx-props'; import { compose } from 'recompose'; import { Observable } from 'rxjs'; import calculateFibonacciExternal from './calculateFibonacci'; const calculateFibonacci = (...args) => Observable.fromPromise(calculateFibonacciExternal(...args)); class FibonacciReactRxProps extends React.Component { //           //     static propTypes = { className: PropTypes.string, value: PropTypes.number, fibonacci: PropTypes.number, }; //,  render() { return ( <div className={ classnames(this.props.className, this.props.loading && 'loading') }> { this.props.loading ? 'Loading...' : `Fibonacci of ${this.props.value} = ${this.props.fibonacci}` } </div> ); } } //compose    HoC    export default compose( //     reactRxProps({ propTypes: { className: PropTypes.string, value: PropTypes.number.isRequired, useServerCall: PropTypes.bool.isRequired, }, }), reactRxPropsConnect({ //   props       propTypes: { className: PropTypes.string, value$: PropTypes.instanceOf(Observable).isRequired, useServerCall$: PropTypes.instanceOf(Observable).isRequired, exist$: PropTypes.instanceOf(Observable).isRequired, }, //      Observables //    , : //this -> model //this.props -> props //this.setState -> render connect: (props, render) => { const model = {}; props.useServerCall$.subscribe(useServerCall => { model.useServerCall = useServerCall; }); props.value$.switchMap(value => { model.value = value; render({ loading: true, }); return calculateFibonacci(model.value, model.useServerCall) .takeUntil(props.exist$); }).subscribe(fibonacci => { render({ loading: false, value: model.value, fibonacci: fibonacci, }); }); }, }) )(FibonacciReactRxProps); 


The component lost its internal state, as well as all the logic associated with Observables, and became elementary for testing, just like the new connect function.

I hope you liked this approach and you also decide to try it. I tried to find libraries with this functionality, but, unfortunately, my search did not return any results.

References:


React Rx Props Library
An example of working with the library

Source: https://habr.com/ru/post/346154/


All Articles