Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / React

Reactive Autonomous States

5.00/5 (3 votes)
27 Nov 2015CDDL7 min read 15.3K  
A practical example of utilizing Reactive Extensions RxJS for autonomous states in React components with greater expressiveness and concise code.

Introduction

In web applications, Reactive Extensions and RxJS can be viewed as "lodash for asynchronous collections", it offers powerful functional constructs and operators to reduce the complexities when web application features scales with the asynchronous nature of web. It also changes programming model from “pull” to “push”, conceptually unifies the data model and behavior across async-array and events, enables effective data transformation with data streams, ultimately simplifies application state management logic with a cleaner approach.

The scope of asynchronous collection includes not only data (array and object, primarily), but also events (keyboard, mouse, gestures, etc.), Promises (Ajax calls, for example) and time based intervals data streams, etc. Although RxJS also runs on Node.js, this article focuses its event streams in user interfaces with React.JS components.

When it comes to asynchronous event processing, RxJS provides greater expressiveness and simplicity; its powerful and abundant operators can transform, translate, filter, aggregate and reconstruct event data when it "flows in the stream". This is the essence of how ‘reactive’ works: observable async data collection serves as data source, transforms data within the stream via operators, while observer subscribes to observable source updates, passes the result to React as component state. React will re-render the component via performant virtual DOM whenever component state updates.

This article is also a follow-up to the "Component Based Web Application". After we selected React  as component model, we’re looking for ways to make intra-component state updates to be more effective and concise. We’ll leave the inter-component interaction (implementing Flux by RxJS) to a different discussion, just focus on the component itself.

Intra-component state is autonomous; it refers to the state that is encapsulated within the component, not passed down from Controller (Container or Mediator) component as immutable properties. Rather, autonomous state is mutable, managed by the component itself, and usually reactive to user input. As we’ll see in the following example, when component has stream-like (continuously updates) and self-contained state data needs to be managed, RxJS’ expressiveness and simplicity is a great help.

The code example is to implement a reusable dropdown menu, it has highlighted menu item that is movable with up/down arrow keys. 

Overview

In the dropdown menu use case, requirements are:

  • when dropdown collapses: up/down arrow keys are enabled to highlight menu item, press enter to execute menu command and close the dropdown
  • any mouse click, escape or tab key will close the dropdown: keyboard and mouse event handling should be stopped

We can certainly implement it by classic DOM addEventListener / removeEventListener, or use SyntheticEvent from React.JS. With RxJS, the code can be cleaner and more expressive.

Here are some basic RxJS constructs and concepts to utilize:

With the help of RxJS, here is our overall solution:

  • Dropdown menu component will has one autonomous state: hightLightIdx, default to -1, represents the index of the menu item that needs to be highlighted, mutable by user inputs
  • Menu items data will be passed in from Controller/Container component as properties, stays immutable
  • One Rx stream for highlighting transforms key presses to hightLightIdx, either up or down
  • Another Rx stream for user input captures all triggers to close dropdown: mouse clicks, tab and escape keys

We’ll set up our component first, initialize the two streams, then attach/detach observers in component life cycle callbacks to make RxJS work with React. Let’s see some code now.

Component Setup

Here is the skeleton of our dropdown menu React component, the default properties show the data structure it expects and default state only has hightLightIdx:

HTML
'use strict';
import React from 'react';
import Rx from 'rx';
const MENU_STYLES = "dropdown-menu-section";

let MenuBarDropDown = React.createClass({
   getDefaultProps() {
      return {
         active: "",
         menuData: {
            label: "",
            items: [
               {
                  label: "", icon: "", action: ""
               }
            ]
         },
         onMenuItemAction: () => {}
      };
   },

   getInitialState() {
      return {
         highLightIdx: -1
      };
   },

   render() {
     let dropDownStyle = this.props.active ? MENU_STYLES + " active" : MENU_STYLES;

     return (
      <ul className={dropDownStyle} role="menu">
         {this.renderMenuItems()}
      </ul>
     );
  },

renderMenuItems() {
   return this.props.menuData.items.map( (item, idx) => {
      if (item.role === 'separator') {
         return (<li key={item.role + idx} role="separator" className="divider"></li>);
      }
      else {
         let itemStyle = this.state.highLightIdx === idx ? 'highlight' : '';
         if (item.disabled) {
            itemStyle = "disabled";
         }
         return (
            <li key={item.action} role="presentation" className={itemStyle}>
               <a href={item.label} onClick={this.onMenuItemAction.bind(this, idx)}>
                  <i className={"icon " + item.icon}></i>{item.label}
               </a>
            </li>
         );
      }
   });
}
});

module.exports = MenuBarDropDown;

Note the active property, it enables the controller/container component to control the collapse or close the drop down. Together with the active CSS class, it enables the dropdown menu component reusable in either a menu-bar container (with multiple instance of drop downs) or as context menu (single instance).

The highLightIdx state is only referenced in renderMenuItems() method, which is invoked from render(), it adds or removes the CSS class of highlight.

Although we haven’t add any Rx code yet, the component is already “reactive” to its state and props updates by React’s re-rendering mechanism. The higher order controller/container component (menu bar or context menu) has no interest on which menu item is highlighted, and how highlighted item is moving up and down, it only cares about which command to execute. This separation of concerns across component boundary makes highLightIdx an autonomous state, entirely encapsulated and managed within the dropdown component itself.

One beautify of this component and reactive patterns is: when extend the component with highlighting behaviors, as we’ll do as next step, all rendering code above stay intact, no code change required to accommodate the new behaviors, open/closed principle can be easily complied.

Let’s extend the component’s behavior by using RxJS now.

Stream Setup

We can write all the data transformation code when we setup the observable streams, these observables will stay idle till subscribe() is invoked. When we’re done with the data stream, we can dispose the subscription what is returned from subscribe() call.

Here is how Rx highlight stream is initialized:

JavaScript
initStream() {
   //base key press stream, relies on event bubbling to the document
   let keyPresses = Rx.Observable.fromEvent(document, 'keyup').map( e => e.keyCode || e.which);

   //both keyUp and keyDown are doing the same: calculate new highLightIdx,
   let upKeys = keyPresses.filter( k => k === 38 ).
      map( (k) => this.getIdxToHighLight('up', this.state.highLightIdx) );

   let downKeys = keyPresses.filter( k => k === 40 ).
      map( (k) => this.getIdxToHighLight('down', this.state.highLightIdx) );

   //so merge them
   this.highLightStream = Rx.Observable.merge(upKeys, downKeys).debounce(100 /* ms */);
}

The highlightStream originates from document.onkeyup, translates to key code, and further transforms to a new calculated value of highLightIdx. When we merge these two streams (upKeys and downKeys) into one, it enables the observer to process the new value of highLightIdx, rather than the key events.

Same idea, we can merge another two streams that would close the dropdown: mouse clicks and tab/escape keys: (the code below is part of the initStream function also):

JavaScript
//base click stream, relies on event bubbling to the document
let mouseClicks = Rx.Observable.fromEvent(document, 'click').map( evt => -1 );
//both escapeKey/tabKey stream and click anywhere also doing the same thing: trigger to close the dropdown
let escapeKeys = keyPresses.filter( k => k === 27 || k === 9).map( k => -1 ); // escape or tab key
let enterKeys = keyPresses.filter(k => k === 13).map(k => this.state.highLightIdx); // enter key


//so, merge them
this.userInputStream = Rx.Observable.merge(mouseClicks, escapeKeys, enterKeys).debounce(100 /* ms */);

When this.userInputStream updates with -1 (mouseClicks, escapeKeys), we need to close the menu and stop processing; While enterKeys updates the stream with current highlighted item index, we need to execute the command identified by the index first, then close the menu.

The above initStream() with two merged Rx streams are ready, we can call it when component is inserted to the DOM:

JavaScript
componentDidMount() {
   this.initStream();
}

The observable streams setup is completed, now let’s look at observers.

Reactive Observers

Both componentDidMount() and componentDidUpdate() are component lifecycle callbacks from React, the former calls initStream() to make the event stream and data transformation ready, and the latter is the hook to trigger subscribe() and dispose() calls: 

JavaScript
componentDidUpdate(prevProps, prevState) {
   if (prevProps.active !== this.props.active) {
      this.handleMenuState(this.props.active === 'active');
   }
}

As we discussed earlier, the active property is passed from controller component to collapse/close the dropdown. All handleMenuState() does is to start/stop the stream processing:

JavaScript
handleMenuState(shown) {   
   if (shown) {
      this.highLightSub = this.highLightStream.subscribe((hIdx) => this.setState({highLightIdx: hIdx }));
      this.userInputSub = this.userInputStream.subscribe((hIdx) => this.onMenuItemAction(this.state.highLightIdx));
   }
   else {
      this.highLightSub.dispose();      
      this.userInputSub.dispose();
   }
}

Now we have a fully functioning dropdown menu, items can be highlighted by arrow keys, command can be executed by clicks or enter key, click outside or press escape/tab key will close the dropdown. We’re almost done except one more case: we need to capture all mouse clicks when menu drops down.

If some DOM elements “swallow the click event”, (like iFrame, or stopPropagation() API calls that cancels event propagation), our mouseClicks stream will not be updated, since it’s attached to bubbled up click event in document. We need to make sure we capture all clicks events so that clicking inside an iframe can also dismiss the collapsed menu.

Tighten Up

There are different ways to capture mouse clicks, the approach we use is a dynamically inserted fixed positioned full screen “backdrop” layer underneath the drop down. The CSS rule is:

CSS
.dropdown-backdrop {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 999;
  background-color: transparent;
}

To set its z-index to be 999 is to make sure the backdrop element is located right behind our drop down menu, which has z-index as 1000:

CSS
.dropdown-menu-section {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 1000;
  display: none;
  float: left;
  min-width: 160px;
  padding: 5px 0;
  margin: 2px 0 0;
  list-style: none;
  font-size: 13px;
  text-align: left;
  background-color: $color-darker-grey;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 3px;
  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
  background-clip: padding-box;

  &.active {
    display: block;
  }

  > li {
    &.highlight {
      background-color: $color-accent;
    }

    &.disabled {
      a {
        color: $color-dark-grey;
        cursor: default;

        &:hover {
          background-color: $color-darker-grey;
          color: $color-dark-grey;
        }
      }
    }

    &.divider {
      height: 1px;
      margin: 6px 0;
      overflow: hidden;
      background-color: $color-dark-grey;
    }

    i {
      width: 20px;
      display: inline-block;
    }

    > a {
      outline: none;
      text-decoration: none;

      display: block;
      padding: 3px 20px;
      clear: both;
      font-weight: normal;
      line-height: 1.42857;
      color: #fff;
      white-space: nowrap;

      &:hover {
        background-color: $color-dark-blue;
        color: $color-lighter;
      }
    }
  }
}

That’s all our CSS rules for dropdown menu and different type of menu items (highlight, disabled and divider).

To create the backdrop element, we can extend the componentDidMount() callback:

JavaScript
componentDidMount() {
   this.initStream();
   this.clickBackDrop = document.createElement('div');
   this.clickBackDrop.className = 'dropdown-backdrop';
},

Now that the backdrop element is ready, we can further extend handleMenuState() to insert / remove the back drop when menu drops down or closes:

JavaScript
handleMenuState(shown) {
   document.activeElement.blur();
   if (shown) {
      document.body.appendChild(this.clickBackDrop);
      this.highLightSub = this.highLightStream.subscribe((hIdx) => this.setState({highLightIdx: hIdx }));
      this.userInputSub = this.userInputStream.subscribe((hIdx) => this.onMenuItemAction(this.state.highLightIdx));
   }
   else {
      this.highLightSub.dispose();      
      this.userInputSub.dispose();
      document.body.removeChild(this.clickBackDrop);
   }
}

Note we didn’t attach event handler to the backdrop element, we only insert it when mouse capturing is desired (menu drops down) and remove it when stream processing needs stop (menu closes).

That’s it.

Wrap up

With the drop down menu example, we find out the state management code is quite clean and straightforward with Rx. No messy event data manipulation within event handlers, no imperative DOM manipulations when rendering component either. Data streams originate from DOM events, effectively transformed to component state, code becomes shorter, cleaner, easy to read and maintain.

RxJS provides an alternative way to handle asynchronous events and data, conceptually unifies event and array/object with stream. Comparing to classic DOM event handling, Reactive Extensions and RxJS has higher level of abstraction and expressiveness, really makes autonomous state management within ReactJS component a breeze.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)