React

React state lifting it up

lift-state-up

How do components share data with each other? The preferred approach is using a state variable in parent component and providing them as props to child components.

Here we will create a metro ticket calculator which will tell if you want to buy a metro ticket or not depending upon your height.

We shall start with a component TicketMachine.

function TicketMachine(props) {
  if (props.height > 29) {
    return <p>Please buy the ticket.</p>;
  }
  return <p>Your ride is free.</p>;
}

Next we will create a component called Metro. It renders an <input> which lets you enter your height and keep its’s value in this.state.height.

It also renders the TicketMachine component showing if you need to buy the ticket or not.

class Metro extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {height: ''};
  }

  handleChange(e) {
    this.setState({height: e.target.value});
  }

  render() {
    const height = this.state.height;
    return (
      <fieldset>
        <legend>Enter height in centimeters:</legend>
        <input
          value={height}
          onChange={this.handleChange} />
        <TicketMachine
          height={parseFloat(height)} />
      </fieldset>
    );
  }
}

Please see it live here. https://codepen.io/anilpank/pen/KKKdbyb

Adding one more input

Now our requirement is for users not comfortable with metric system we will provide them an option to provide them their height in feet.

Let’s extract HeightInput component from Metro component. We shall add a new scale prop to it that can either be cm or ft.

const scaleNames = {
  cm: 'Centimeter',
  ft: 'Feet'
};

class HeightInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {height: ''};
  }

  handleChange(e) {
    this.setState({height: e.target.value});
  }

  render() {
    const height= this.state.height;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter height in {scaleNames[scale]}:</legend>
        <input value={height}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

We can now change Metro to render two separate height inputs.

class Metro extends React.Component {
  render() {
    return (<div>
               <HeightInput scale="cm"/>
               <HeightInput scale="ft"/>
           </div>);
  }
}

We have two inputs now. But when you enter height in one of them, other one does not change. But our requirement is when we change even one of them, the other one should change automatically.

Let’s address this problem. First let’s write few conversion functions for converting height from centimeters to feet and vice versa.

function toCentiMeter(ft) {
  return ft*30.48;
}

function toFeet(cm) {
  return  cm/30.48;
}

function tryConvert(height, convert) {
  const input = parseFloat(height);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

Lift State Up

Currently both HeightInput components have their own state variables where they keep their height values. But our requirement is to keep both of them in sync. So if user changes one value, the other should be automatically changed.

This can be accomplished by moving state to a closest common parent. So we will remove local state from HeightInput component to Metro component. So now your Metro component is single source of truth.

render() {
    // Before: const temperature = this.state.height;
    const temperature = this.props.height;
    // ...

As you know that props are always read only. When height was an internal state variable for HeightInput component it was easy to change it using this.setState anywhere in the component. Now height is coming from parent as a prop. This is solved by making the HeightInput component as controlled component. It needs to accept a value and an onChange prop.

Now when HeightInput component wants to change it’s height it calls the prop this,props.onHeightChange

handleChange(e) {
    // Before: this.setState({height: e.target.value});
    this.props.onHeightChange(e.target.value);
    // ...
class HeightInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onHeightChange(e.target.value);
  }

  render() {
    const height = this.props.height;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter height in {scaleNames[scale]}:</legend>
        <input value={height}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

Now let’s turn to Metro component. We shall store current input’s height and scale in local state.

class Metro extends React.Component {
  constructor(props) {
    super(props);
    this.handleCentimeterChange = this.handleCentimeterChange.bind(this);
    this.handleFeetChange = this.handleFeetChange.bind(this);
    this.state = {height: '', scale: 'cm'};
  }

  handleCentimeterChange(height) {
    this.setState({scale: 'cm', height});
  }

  handleFeetChange(height) {
    this.setState({scale: 'ft', height});
  }

  render() {
    const scale = this.state.scale;
    const height = this.state.height;
    const centimeter = scale === 'ft' ? tryConvert(height, toCentiMeter) : height;
    const feet = scale === 'cm' ? tryConvert(height, toFeet) : height;

    return (
      <div>
        <HeightInput
          scale="cm"
          height={centimeter}
          onHeightChange={this.handleCentimeterChange} />

        <HeightInput
          scale="ft"
          height={feet}
          onHeightChange={this.handleFeetChange} />

        <TicketMachine
          height={parseFloat(centimeter)} />

      </div>
    );
  }
}

Now no matter what you edit, this.state.height and this.state.height in Metro component gets updated.

See it live in action here. https://codepen.io/anilpank/pen/QWWyLxw

Few important things to keep in mind. That there must be single source of truth for anything that changes in a React application. Generally state is added to a component that needs it for rendering. Then if other components need to access or change the same value, then it is lifted up to closest common parent. One common complaint from React developers on this is that it involves writing a lot of boilerplate code. But coming from an AngularJS 1.x background with two way data binding, it is much easier to find a bug in React when we are using this concept of lifting state up rather than in Angularjs 1.x two way data binding approach.

Pro Tip – Whenever you see something wrong in UI, open the React Developer Tools, inspect the props and keep moving to component’s parents and ancestors till you find the culprit component responsible for updating the state.

How useful was this post?

Click on a star to rate it!

Average rating / 5. Vote count:

As you found this post useful...

Follow us on social media!

We are sorry that this post was not useful for you!

Let us improve this post!

About anil

Hi, I'm Anil Verma. I have a passion for teaching and developing awesome front end apps. I was involved in development of post processor for Nastran(built by NASA). More recently I have been building supply chain apps at E2open.
View all posts by anil →

Leave a Reply

Your email address will not be published. Required fields are marked *