DEV Community

Cover image for RxJS in React - From Class  to Functional - Part 1 Class Components
Eyal Lapid
Eyal Lapid

Posted on • Updated on

RxJS in React - From Class to Functional - Part 1 Class Components

Image is from Pixabay user kordi_vahle

React functional component and hooks pose a slight different model of operation then the standard component lifecycle model.

In order to use RxJS effectively in React functional components with hooks, we need to adjust some of the pattern we use in regular components to the new paradigm.

This is part 1 of the series. It deals with RxJS patterns for React Class components.

Part 2 will handle React functional components patterns and what is the logic behind them.

RxJS Logo

 

RxJS in Regular Class Components

Component Lifecycles

In regular components frameworks, there is an on init lifecycle that runs once, on change lifecycle that runs on each component's input change, and on destroy lifecycle that get called when the component is removed and discarded.

The analog to those lifecycles in React are:

class Calculator {
  constructor(props) {
    // ... 
  }

  componentDidMount() {
    // ...
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // ... 
  }

  componentWillUnmount() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

 

Using RxJS with Class Component Lifecycles

Initialization

The componentDidMount callback and the constructor allow to create the main streams that will guide the component logic and bring them to life - to subscribe to them.

class Calculator {
  constructor(props) {
    super(props);

    /*
     * Initialization of callbacks fucntions 
     * to send stream events
     */
    this.onNumber = (num) => this.numAction$.next(num);
    this.onMathOperation = (operation) => this.mathOperation$.next(operation);

    /*
     * Initialize reflected state of the main streams values.
     * To be used in the view layer.
     */
    this.state = {
      result: "",
      expr: []
    };
  }

  componentDidMount() {
    /* 
     * Setting up the event Rx subjects. Will be used in 
     * the main streams.
     */
    this.onUnmount$ = new Subject();
    this.numAction$ = new Subject();
    this.mathOperation$ = new Subject();

    /* 
     * Setting up the main logic streams.
     */
    this.mathExpression$ = this.streamMathExpression();
    this.result$ = this.streamResult();

    /* 
     * 1. Reflecting the main streams values into
     *    regular state to be used in the view.
     * 2. Subscribing to the main streams to bring
     *    them to life for the duration of the 
     *    component.
     */
    merge(
      this.result$.pipe(
        tap((result) => this.setState((state) => ({ ...state, result })))
      ),
      this.mathExpression$.pipe(
        tap((expr) => this.setState((state) => ({ ...state, expr })))
      )
    )
      .pipe(takeUntil(this.onUnmount$))
      .subscribe();
  }

 /* 
  * Main stream expression
  */
 streamMathExpression() {
    return pipe(
      () =>
        merge(
          this.numAction$.pipe(
            map((num) => ({ action: "number", value: num }))
          ),
          this.mathOperation$.pipe(
            map((op) => ({ action: "operation", value: op }))
          )
        ),
      scan((expr, { action, value }) => {
        // reducer logic for the math expression
      }, [])
    )(null);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, in the init phase of the component, several things happening:

  1. Main observables values reflection into React component state.
  2. Creating Rx Subjects to be used and notify the main streams of changes and events.
  3. Creating and wiring up the main streams that handle the component behavior and state management.
Using Values in the View

This is needed for using the observable values in the view jsx layer. In Angular for instance, this is done with the async pipe that subscribe and provide the observable value automatically in Angular HTML template expressions.

Initialization of React component state in the constructor:

constructor() {
  this.state = {
      result: "",
      expr: []
    };
Enter fullscreen mode Exit fullscreen mode

Reflecting and updating the main observables values into the React component states. This is done with subscribing to the observables and updating the React component state.

componentDidMount() {
merge(
      this.result$.pipe(
        tap((result) => this.setState((state) => ({ ...state, result })))
      ),
      this.mathExpression$.pipe(
        tap((expr) => this.setState((state) => ({ ...state, expr })))
      )
    )
      .pipe(takeUntil(this.onUnmount$))
      .subscribe();
}
Enter fullscreen mode Exit fullscreen mode
Creating Rx Subjects to be used and notify the main streams of changes and events

The main streams subscribe to the subjects that are used in the view or in other stream to notify of changes or events such as click and timer events.

constructor(props) {
    super(props);

    this.onNumber = (num) => this.numAction$.next(num);
    this.onMathOperation = (operation) => this.mathOperation$.next(operation);
}
 componentDidMount() {
    this.onUnmount$ = new Subject();
    this.numAction$ = new Subject();
    this.mathOperation$ = new Subject();
}
Enter fullscreen mode Exit fullscreen mode
Main Streams Logic

The main streams of the component. Those needs to be created and brought to life - subscribed to - only once and live for the rest of the component life until it destroyed.

componentDidMount() {
    this.mathExpression$ = this.streamMathExpression();
    this.result$ = this.streamResult();

    merge(
      this.result$.pipe(
        // ...
      ),
      this.mathExpression$.pipe(
        // ...
      )
    )
      .pipe(takeUntil(this.onUnmount$))
      .subscribe();
}
Enter fullscreen mode Exit fullscreen mode

Input Change Detection

The onChange callback allows to push detected input changes into RxJS subjects. The main streams of the component then can listen on those subject and react to component input changes.

Gathering the changes events:

componentDidMount() {
  this.propsChanges$ = new Subject();
}

componentDidUpdate(prevProps) {
    this.propsChanges$.next({ prev: prevProps, current: this.props });
}
Enter fullscreen mode Exit fullscreen mode

Listening and notifying relevant prop changes:

 componentDidMount() {
    this.inputMode$ = handlePropChange("mode", this.propsChanges$);

    merge(
      this.inputMode$,
    )
      .pipe(takeUntil(this.onUnmount$))
      .subscribe();
  }
Enter fullscreen mode Exit fullscreen mode
function handlePropChange(propName, propsChanges$) {
  return propsChanges$.pipe(
    filter(({ prev, current }) => prev[propName] !== current[propName]),
    map(({ current }) => current[propName]),
    shareReplay({ bufferSize: 1, refCount: true })
  );
}
Enter fullscreen mode Exit fullscreen mode

Cleanup

The componentWillUnmount callback allows to cleanup subscriptions of the main logic streams so no dangling subscriptions will be left after the component is destroyed.

This is done through another subject - onUnmount - that all other main streams subscription depends on so when it emit and complete, the subscriptions of the main streams complete as well.

The takeUntil operator subscribe to the unMount$ subject and complete the main subscriptions when unMount complete.

componentDidMount() {
    this.onUnmount$ = new Subject();
    this.mathExpression$ = this.streamMathExpression();
    this.result$ = this.streamResult();

    merge(
      this.result$.pipe(
        // ...
      ),
      this.mathExpression$.pipe(
        // ...
      )
    )
      .pipe(takeUntil(this.onUnmount$))
      .subscribe();
  }

 componentWillUnmount() {
    this.onUnmount$.next();
    this.onUnmount$.complete();
  }

Enter fullscreen mode Exit fullscreen mode

Full example code can be found on github;

Conclusion

This part dealt with RxJS patterns for React class components. Specifically we saw:

  1. How to create main streams for the duration of the component life.
  2. Use the streams values in the view layer.
  3. Listen on component input props changes.
  4. Cleanup subscriptions when the component is destroyed.

Next part will show how to update those patterns to React functional components with hooks and the logic behind those patterns.

Discussion (0)