Building a Unique check with React

April 11, 2017

Shopify has a fancy instant Front-End check when registering a new store to make sure the name is unique before you even submit the form. I wanted to build this as a React Component for a new project I’ve been working on.

What we will be building

The React Component

The first step is to define the basic of the component, we will be storing the company_name as they input it and whether or not the name is unique,
company_name_valid.

import React, { Component } from 'react';

export default class RegisterCompany extends Component {  
  constructor(props) {
    super(props);

    this.state = {
        company_name: '',
        company_name_valid: null, // null - have not checked, false - not unique, true - unique
    }

    this.inputChange    = this.inputChange.bind(this);
  }

  inputChange(e){
    this.setState({company_name: e.target.value, company_name_valid: null});
  }

  render(){
    return( 
      <div>
        <div className="card">
          <div className="card-header">
            Register a Business
          </div>
          <div className="card-block">
            <div className="form-group">
              <span className="input-icon">
                 <i className="fa fa-fw fa-id-badge"></i>
              </span>
              <input type="text" id="email" placeholder="Business Name" onChange={this.inputChange} value={this.state.company_name} className={`form-control ${this.state.company_name_valid ? 'form-control-success' : (this.state.company_name_valid === false ? 'form-control-danger' : '') }`} />
              <span className="input-action">
                  <i className="fa fa-spinner fa-pulse"></i>
              </span>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

This class now simply displays the input field and stores the value of company_name when the user types. We now need to check if it unique via an API call to our application.

import axios from 'axios';

export default class RegisterCompany extends Component {  
  constructor(props) {
    super(props);

    this.state = {
        ...
        fetching: false
    }

    ...
    this.inputCheck     = this.inputCheck.bind(this);
  }

  ...

  inputKeyUp(e){
    this.inputCheck(e.target.value);
  }

  inputCheck(){
    this.setState({company_name_valid: null});
    if(this.state.company_name !== ''){
      axios.get('businesses/'+this.state.company_name+'/check')
      .then(response => {
          return response.data;
      })
      .then(data => {
          this.setState({ company_name_valid: data.valid, fetching: false });
      });
    }
  }

  render(){
    return( 
      <div>
        ...
              <input type="text" id="email" placeholder="Business Name" onChange={this.inputChange} onKeyUp={this.inputKeyUp} value={this.state.company_name} className={`form-control ${this.state.company_name_valid ? 'form-control-success' : (this.state.company_name_valid === false ? 'form-control-danger' : '') }`} />
      </div>
    );
  }
}
We use axios as our AJAX library.

In the above example we are now checking on every single key up that the user enters, which means if they type New Business there will be 12 API calls to check each new string, first N then Nestops typing for an extended period of time (extended being maybe half of a second).

This is where a Debounce library comes in handy. After doing a

yarn add throttle-debounce --dev

we can then add a debounce to the inputCheck of 500ms like;

import debounce from 'throttle-debounce/debounce'

export default class RegisterCompany extends Component {  
  constructor(props) {
    ...
    this.inputCheck     = debounce(500, this.inputCheck);
  }

  inputKeyUp(e){
    this.inputCheck(e.target.value);
  }

  inputCheck(){
    this.setState({company_name_valid: null});
    if(this.state.company_name !== ''){
      axios.get('businesses/'+this.state.company_name+'/check')
      .then(response => {
          return response.data;
      })
      .then(data => {
          this.setState({ company_name_valid: data.valid, fetching: false });
      });
    }
  }

  render(){
    return( 
      <div>
        ...
              <input type="text" id="email" placeholder="Business Name" onChange={this.inputChange} onKeyUp={this.inputKeyUp.bind(this)} value={this.state.company_name} className={`form-control ${this.state.company_name_valid ? 'form-control-success' : (this.state.company_name_valid === false ? 'form-control-danger' : '') }`} />
      </div>
    );
  }
}
The keyUp event will still trigger on each keyUp, but the inputCheck function will only be called if the inputKeyUp event has not been fired in the past 500ms.

Almost there!

The last thing we need to do now is to display some feedback to the user based on the API response. We will do this by adding a form_class to the form and some custom output to display the text response. We also want to show the submit button if the name is valid so the user can submit it.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import debounce from 'throttle-debounce/debounce'
import axios from 'axios';

import SubmitButton from './SubmitButton'

export default class RegisterCompany extends Component {  
  constructor(props) {
    super(props);

    this.state = {
        company_name: '',
        company_name_valid: null,
        form_class: '',
        fetching: false
    }

    this.inputChange    = this.inputChange.bind(this);
    this.inputCheck     = debounce(650, this.inputCheck);
    this.submitRegister = this.submitRegister.bind(this);
  }

  nameLabel(){
    if (this.state.company_name_valid) {
      return (
        <div className="form-control-feedback">That Business name is available!</div>
      )
    }
    else if (this.state.company_name_valid === false) {
      return (
        <div className="form-control-feedback">That Business name has already been taken</div>
      )
    }
  }

  ifValidNameFields(){
    if (this.state.company_name_valid) {
      return (
        <div className="valid-name">
          <SubmitButton text="Register Business" className="btn btn-primary btn-block" submitting={this.state.fetching} clicked={this.submitRegister}></SubmitButton>
        </div>
      )
    }
  }

  inputChange(e){
    this.setState({company_name: e.target.value, company_name_valid: null, form_class: ''});
  }

  inputKeyUp(e){
    this.inputCheck(e.target.value);
  }

  inputCheck(){
    this.setState({company_name_valid: null, form_class: 'has-loading'});
    if(this.state.company_name !== ''){
      axios.get('businesses/'+this.state.company_name+'/check')
      .then(response => {
          return response.data;
      })
      .then(data => {
          this.setState({ company_name_valid: data.valid, fetching: false, form_class: data.valid ? 'has-success' : 'has-danger' });
      });
    }
  }

  submitRegister(){
    this.setState({fetching: true});
    axios.post('businesses', {
      name: this.state.company_name
    })
    .then(function (response) {
      window.location = '/business/account/'+response.data.slug;
    })
    .catch(function (error) {
      console.log(error);
    });
  }

  render(){
    return( 
      <div>
        <div className="card">
          <div className="card-header">
            Register a Business
          </div>
          <div className="card-block">
            <div className={`form-group ${this.state.form_class}`}>
              <span className="input-icon">
                 <i className="fa fa-fw fa-id-badge"></i>
              </span>
              <input type="text" id="email" placeholder="Business Name" onChange={this.inputChange} onKeyUp={this.inputKeyUp.bind(this)} value={this.state.company_name} className={`form-control ${this.state.company_name_valid ? 'form-control-success' : (this.state.company_name_valid === false ? 'form-control-danger' : '') }`} />
              <span className="input-action">
                  <i className="fa fa-spinner fa-pulse"></i>
              </span>
              { this.nameLabel() }
            </div>
            { this.ifValidNameFields() }
          </div>
        </div>
      </div>
    );
  }
}
A gist of this along with the SubmitButton Component can be found here