ReactJS Custom Radio Buttons

Started to work on a project based on ReactJS and from previous (probably bad) experience started to look for a third party component for Radio Buttons.

After not finding a suitable solution I quickly implemented the following tiny component for making any button-like elements switch like radio buttons.

Usage

The way we would like to use the radio buttons is by defining them hierarchically. We should be able to collect the radio buttons into groups and these buttons should become connected like native radio inputs fields.

class Application extends React.Component {
  render() {
    return (
      <RadioGroup name="test">
        <Radio value="1" />
        <Radio value="2" />
      </RadioGroup>
    );
  }
}

ReactDOM.render(
  <Application />,
  document.getElementById('app')
);

Components

The main Radio component will have a selected state, which is a binary and a setSelected method to set the selected state of the Radio button. It also receives the parent (RadioGroup) in the context.

class Radio extends React.Component {
  constructor(props) {
    super(props);
    // The state contains only the selected
    // property, which is true when selected.
    this.state = {selected: false};
  }

  /**
   * Toggles the state of the Radio component and
   * calls the onChange method of wrapping RadioGroup
   * component. New selected state and the Radio component 
   * itself are passed to onChange method.
   */
  toggle() {
    const {onChange} = this.context.radioGroup;
    const selected = !this.state.selected;
    this.setState({selected});
    onChange(selected, this);
  }
  
  /**
   * Sets the current selected state of the component.
   *
   * @param {boolean} selected - Selected state
   */
  setSelected(selected) {
    this.setState({selected});
  }
  
  /**
   * Renders the component.
   */
  render() {
    let classname = this.state.selected ? 'active' : '';
    return (
      <button type="button" className={classname}
          onClick={this.toggle.bind(this)}>
      </button>
    );
  }
}

/**
 * Defines the context types, so we receive the
 * context from parent RadioGroup component.
 */
Radio.contextTypes = {
  radioGroup: React.PropTypes.object
};

The parent component - RadioGroup should inject itself and a change handler to Radio component and track the changes so when one is selected others are reset automatically.

class RadioGroup extends React.Component {
  constructor(props) {
    super(props);
    // The list of child components.
    // Loaded from the render method.
    this.options = [];
  }

  /**
   * Builds a context for child element. Context contains
   * the name of the group and a handler for change event.
   * 
   * @return {Object} context
   */
  getChildContext() {
    const {name} = this.props;
    return {radioGroup: {
      name,
      onChange: this.onChange.bind(this)
    }};
  }
  
  /**
   * Event handler for child components. Sets all the
   * other child components to their reverse state. 
   *
   * @param {boolean} selected - New selected state
   * @param {React.Component} child - Radio component
   */
  onChange(selected, child) {
    this.options.forEach(option => {
      if (option !== child) {
        option.setSelected(!selected);
      }
    });
  }
  
  /**
   * Renders the component and fills the options property
   * with child component instances.
   */
  render() {
    let children = React.Children.map(this.props.children, child => {
      return React.cloneElement(child, {
        ref: (component => {this.options.push(component);}) 
      });
    });
    return <div className="radio-group">{children}</div>;
  }
}

/**
 * Defines the context types for its children.
 */
RadioGroup.childContextTypes = {
  radioGroup: React.PropTypes.object
};

The complete example can be found at Code Pen and in the embedded Pen below.