Create radio group component in react

In the previous article we had seen how to create checkbox in react, Now we will see how to create radio box component in react.

Unlike checkbox, radio cannot be a single element, they need to be in a group because either one can only be selected, there cannot be multiple selected values.

So this component will generate a group of radio boxes by taking array of options.

Following is the list of props we will need in our component.

options: PropTypes.arrayOf(
  PropTypes.shape({
    value: PropTypes.any.isRequired,
    label: PropTypes.string.isRequired,
    disabled: PropTypes.bool
  })
).isRequired,
name: PropTypes.string.isRequired,

/* Will be triggered when there is change in selected optiomn */
onChange: PropTypes.func,

/* Applied when the component is not controlled*/
prefillValue: PropTypes.any,

/*Applies to value of the selected option*/
value: PropTypes.any,

/* All radio items will be inline-flexed if this is true */
inline: PropTypes.bool,

/* Applied to container of all radio items */
className: PropTypes.string,

/* Applies to each item in this group */
itemClassName: PropTypes.string,

/* Applies to the label of each item in this group */
labelClassName: PropTypes.string,

/* Applies to actual input */
inputClassName: PropTypes.string,

/* Applies to circle part of radio button */
customLabelClassName: PropTypes.string,

/* ref of the component */
ref: PropTypes.instanceOf(Element)

These are some of the minimal props we expect in our component. You can add more and extend the component as per your requirements.

In options we are expecting an array of objects in the specified shape through which we will generate the radio group.

Our radio component will be made up of three different parts

  • Label: Text which will be displayed along the radio button.
  • Radio input type: This will be invisible.
  • Overlay: This will placed above the invisible radio input and we will use this for styling.

Now as you have got some good understanding of the radio component, let us start creating it.

We will be using some extra packages for our help.

  • prop-types: This will be used to validate the props we are about receive to make sure we get exactly what we want.
  • classnames: This helps us to use CSS classes as Javascript object inside our component which helps us to dynamically add styles with ease. To make this work you just need to name the CSS file as filename.module.css.

Following is the folder structure of our component.
React radiobox component folder structure

First, import all the required packages at the top.

import React, { Component } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import styles from "./index.module.css";

class RadioGroup extends Component {
  //other codes will go here...
}

export default RadioGroup;

As we need to maintain the state in the component in order to update the selected radio input which is why we are creating a class.

Check the props initially before proceeding any further.

static propTypes = {
    options: PropTypes.arrayOf(
      PropTypes.shape({
        value: PropTypes.any.isRequired,
        label: PropTypes.string.isRequired,
        disabled: PropTypes.bool
      })
    ).isRequired,

    name: PropTypes.string.isRequired,

    /* Will be triggered when there is change in selected optiomn */
    onChange: PropTypes.func,

    /* Applied when the component is not controlled*/
    prefillValue: PropTypes.any,

    /*Applies to value of the selected option*/
    value: PropTypes.any,

    /* All radio items will be inline-flexed if this is true */
    inline: PropTypes.bool,

    /* Applied to container of all radio items */
    className: PropTypes.string,

    /* Applies to each item in this group */
    itemClassName: PropTypes.string,

    /* Applies to the label of each item in this group */
    labelClassName: PropTypes.string,

    /* Applies to actual input */
    inputClassName: PropTypes.string,

    /* Applies to circle part of radio button */
    customLabelClassName: PropTypes.string,

    /* ref of the component */
    ref: PropTypes.instanceOf(Element)
  };

  static defaultProps = {
    itemClassName: "",
    className: "",
    labelClassName: ""
  };

These are the static methods for props validation and setting the defaults.

Set up the state to store the change of values. We will do it inside the constructor. But you don’t necessarily have to do it inside it. You can declare the state directly as well.

constructor(props) {
    super(props);
    
    //Set the value if passed
    this.state = {
      value: props.prefillValue || props.value
    };
  }

If you have set any predefined value then assign it to the state, else assign the received value from the parent if the component is controlled.

Now we will render our component with different parts and assign the specific classes to them.

render() {
    const {
      itemClassName,
      className,
      labelClassName,
      name,
      inline,
      options,
      inputClassName,
      customLabelClassName
    } = this.props;

    const { value } = this.state;

    const _itemClassName = cx(itemClassName, styles.item, {
      [styles.inlineItem]: inline
    });

    const _labelClassName = cx(labelClassName, styles.label);

    const _inputClassName = cx(styles.radio, inputClassName);

    const _customLabelClassName = cx(styles.customLabel, customLabelClassName);

    return (
      <div className={className} ref={this.props.ref}>
        
        {options.map((option, index) => (
          <div
            className={cx(_itemClassName, {
              [styles.disabled]: option.disabled
            })}
            key={index}
          >
            <input
              type="radio"
              name={name}
              id={`${name}-${option.value}`}
              value={option.value}
              onChange={this.handleOnChange}
              checked={value === option.value}
              className={_inputClassName}
            />

            <label
              htmlFor={`${name}-${option.value}`}
              className={_customLabelClassName}
            ></label>

            <label
              htmlFor={`${name}-${option.value}`}
              className={_labelClassName}
            >
              {option.label}
            </label>

          </div>
        ))}

      </div>
    );
  }

As you can see we are mapping the options and generating the list of radio boxes from it.

Now it is time to handle the changes.

You may have noticed in the props that we are receiving ref from the parent, this way parent can forward his ref and control the child with explicitly listening to events from the child.

But we will be using old method which easier to understand. We will use an event handler and when ever value changes and if parent want to listen the changes we will notify it to the parent else update the inner state.

handleOnChange = e => {
  const { name, onChange } = this.props;
  const { value } = e.target;

  if (onChange) {
    onChange({ name, value });
  } else {
    this.setState({ value });
  }
};

Here after updating, if the parent is controlling the component then we will be receiving the updated value in the props.

So we need to make sure that if the received value and current value in the state are different then only update component.

componentDidUpdate(previousProps) {
  const { value } = this.props;
  //If value is different then only update the state
  if (previousProps.value != this.props.value) {
    this.setState({ value });
  }
}

Complete code of radio group component in react

import React, { Component } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import styles from "./index.module.css";

class RadioGroup extends Component {
  static propTypes = {
    options: PropTypes.arrayOf(
      PropTypes.shape({
        value: PropTypes.any.isRequired,
        label: PropTypes.string.isRequired,
        disabled: PropTypes.bool
      })
    ).isRequired,

    name: PropTypes.string.isRequired,

    /* Will be triggered when there is change in selected optiomn */
    onChange: PropTypes.func,

    /* Applied when the component is not controlled*/
    prefillValue: PropTypes.any,

    /*Applies to value of the selected option*/
    value: PropTypes.any,

    /* All radio items will be inline-flexed if this is true */
    inline: PropTypes.bool,

    /* Applied to container of all radio items */
    className: PropTypes.string,

    /* Applies to each item in this group */
    itemClassName: PropTypes.string,

    /* Applies to the label of each item in this group */
    labelClassName: PropTypes.string,

    /* Applies to actual input */
    inputClassName: PropTypes.string,

    /* Applies to circle part of radio button */
    customLabelClassName: PropTypes.string,

    /* ref of the component */
    ref: PropTypes.instanceOf(Element)
  };

  static defaultProps = {
    itemClassName: "",
    className: "",
    labelClassName: ""
  };

  constructor(props) {
    super(props);

    this.state = {
      value: props.prefillValue || props.value
    };
  }

  componentDidUpdate(previousProps) {
    const { value } = this.props;
    //If value is different then only update the state
    if (previousProps.value != this.props.value) {
      this.setState({ value });
    }
  }

  handleOnChange = e => {
    const { name, onChange } = this.props;
    const { value } = e.target;

    if (onChange) {
      onChange({ name, value });
    } else {
      this.setState({ value });
    }
  };

 render() {
    const {
      itemClassName,
      className,
      labelClassName,
      name,
      inline,
      options,
      inputClassName,
      customLabelClassName
    } = this.props;

    const { value } = this.state;

    const _itemClassName = cx(itemClassName, styles.item, {
      [styles.inlineItem]: inline
    });

    const _labelClassName = cx(labelClassName, styles.label);

    const _inputClassName = cx(styles.radio, inputClassName);

    const _customLabelClassName = cx(styles.customLabel, customLabelClassName);

    return (
      <div className={className} ref={this.props.ref}>
        
        {options.map((option, index) => (
          <div
            className={cx(_itemClassName, {
              [styles.disabled]: option.disabled
            })}
            key={index}
          >
            <input
              type="radio"
              name={name}
              id={`${name}-${option.value}`}
              value={option.value}
              onChange={this.handleOnChange}
              checked={value === option.value}
              className={_inputClassName}
            />

            <label
              htmlFor={`${name}-${option.value}`}
              className={_customLabelClassName}
            ></label>

            <label
              htmlFor={`${name}-${option.value}`}
              className={_labelClassName}
            >
              {option.label}
            </label>

          </div>
        ))}

      </div>
    );
  }
}

export default RadioGroup;

We have finished structuring our component, lets do some styling now.

Styling is little complicated as we will be using sibling selectors to create the overlay over the radio input for both checked and unchecked state.

Complete css code for radio group.

//index.module.css
.radio {
  display: none;
}

//Sibling selector +
.radio:checked + .customLabel {
  border-color: #68c721;
}

.radio:checked + .customLabel:after {
  content: "";
}

.customLabel {
  border: 1px solid #a8acb1;
  background-color: #fff;
  border-radius: 50%;
  min-width: 20px;
  min-height: 20px;
  cursor: pointer;
}

.customLabel::after {
  width: 10px;
  height: 10px;
  background: #68c721;
  border-radius: 50%;
  display: block;
  margin-top: 4px;
  margin-left: 4px;
}

.white .radio:checked + .customLabel {
  background: transparent;
  border-color: #a8acb1;
}

.white .radio:checked + .customLabel::after {
  border-color: #2d2d2d;
}

.item {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
}

.item.disabled {
  opacity: 0.3;
}

.item.disabled .label,
.item.disabled .customLabel {
  cursor: default;
}

.inlineItem {
  display: inline-flex;
}

.label {
  cursor: pointer;
  margin-left: 15px;
  font-size: 14px;
}

Input

ReactDOM.render(
  <div className="abc">
    <RadioGroup
      options={[
        { label: "I am not checked", value: "xyz", disabled: false },
        { label: "I am checked", value: "abc", disabled: false }
      ]}
      name="radio"
      prefillValue="abc"
      inline={true}
    />
  </div>,
  document.getElementById("root")
);

Output

React Radio group