Using React: Part 4: Handling Events

Learning React

Using React

React is a popular JavaScript library for creating user interfaces. It combines many features of JavaScript ES6 together to create a powerful way to quickly create front-end functionality.


Handling Events

In the last part, in-line events were used. In the Item object, an onClick() event handler was added to an HTML element. Within it, an arrow function was used to call the setState() function to change a value and re-call the render() function to refresh the visual result.


import React from 'react';
import Checkmark from './Checkmark.js'

class Item extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      isDone: false
    };

  }

  render() {

    let item;

    if(this.state.isDone) {

      item = <Checkmark />;

    } else {

      item = <input
        type="checkbox"
        name={this.props.number}
        onClick={() => this.setState({isDone: true})}
        />;

    }

    return (
      <li>{item} TODO!</li>
    );
  }
}

export default Item;

To extend the ShoppingList object, a button should be added so that users can add new items for them to buy.

ShoppingList.js


import React from 'react';
import Item from './Item.js';
import AddButton from './AddButton.js';

class ShoppingList extends React.Component {

  createItem(x) {
    return (
      <Item number={x} />
    )
  }

  render() {
    return (
      <div>
        <div><AddButton /></div>
        <ul>
        {this.createItem(1)}
        {this.createItem(2)}
        {this.createItem(3)}
        </ul>
      </div>
    );
  }
}

export default ShoppingList;

AddButton.js


import React from 'react';

class AddButton extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      items: []
    };

  }

  addItem() {
     const items = this.state.items.concat("New Item");

     this.setState({items: items});
  }

  render() {
    return(
      <button onClick={() => this.addItem()} >+</button>
    );
  }
}

export default AddButton;

In the new AddButton class, a function addItem() is used to add new items to the existing ones. Because we should not directly change values on the this.state property, the use of the array function concat() allows for saving the changed array (returned from the function use) and then update the this.state.items array with a new array containing the added value.

However, there is a problem. Adding new items to this class, only adds them to the class. As a component, it is enclosed, and adding new items to only this class does not help the overall interface.

Lifting State

Designing projects in React has a concept called “lifting state.” This occurs when the state of a component needs to be moved to its “parent” element so that other components can either have access or it should update something it does not have access to change.

In this example, the “state” in question should be lifted to the ShoppingList class. Adding items should happen here even if the action is happening in another class.

As properties can be sent to an element via the props value, this allows for a new usage of sending an event handler via this object. Through writing a function on a “parent” and then invoking a class, a function can be passed along to be called from the “child” that is actually part of the “parent.” However, this also needs some explanation.

Normally, when a function is called, it is run within the current scope and has the this of that scope. This allows for functions within a class to reference its own this and have internal consistency. To have a “child” run a function within its “parent” scope, the use of the function bind() is needed to control which this the function uses.

ShoppingList.js


import React from 'react';
import Item from './Item.js';
import AddButton from './AddButton.js';

class ShoppingList extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      items: []
    };

    // Since addItem will be called within another scope,
    //  we bind() it to the 'this' inside ShoppingList.
    this.addItem = this.addItem.bind(this);

  }

  addItem() {

    const items = this.state.items.concat("New Item");

    this.setState({items: items});

  }

  createItem(x) {
    return (
      <Item number={x} />
    )
  }

  render() {
    return (
      <div>
        <div><AddButton onClick={this.addItem} /></div>
        <ul>
        {this.createItem(1)}
        {this.createItem(2)}
        {this.createItem(3)}
        </ul>
      </div>
    );
  }
}

export default ShoppingList;

AddButton.js


import React from 'react';

class AddButton extends React.Component {

  render() {
    return(
      <button onClick={this.props.onClick} >+</button>
    );
  }
}

export default AddButton;

Now, clicking on the BUTTON as part of the AddButton class will call the ShoppingList.addItem() function because it was passed to the AddButton class via the props value. It is a “child” calling its “parent’s” function, and with the use of bind(), it is also running in the “parent” scope. Even though the action is taking place in one component, another is handling it.

Cleaning up ShoppingList and Item

While work was being done to “lift state” out of AddButton and into ShoppingList, the render() function was still using the createItem() function. At this point, that can be removed and the code cleaned up to use the new internal array this.state.items that will be updated through the AddButton class.

ShoppingList.js


import React from 'react';
import Item from './Item.js';
import AddButton from './AddButton.js';

class ShoppingList extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      items: []
    };

    // Since addItem will be called within another scope,
    //  we bind() it to the 'this' inside ShoppingList.
    this.addItem = this.addItem.bind(this);

  }

  addItem() {

    const items = this.state.items.concat("New Item");

    this.setState({items: items});

  }

  render() {
    return (
      <div>
        <div><AddButton onClick={this.addItem} /></div>
        <ul>
        {this.state.items.map((item, index) => (
              <Item name={item} key={index} />
        ))}
        </ul>
      </div>
    );
  }
}

export default ShoppingList;

Item.js


import React from 'react';
import Checkmark from './Checkmark.js'

class Item extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      isDone: false
    };

  }

  render() {

    let item;

    if(this.state.isDone) {

      item = <Checkmark />;

    } else {

      item = <input
        type="checkbox"
        onClick={() => this.setState({isDone: true})}
        />;

    }

    return (
      <li>{item} {this.props.name}</li>
    );
  }
}

export default Item;

In ShoppingList, the map() function can be used to move through the array and call Item and pass it to two values via props: name and key.

Internally, in Item, the value of this.props.name is used as its new value between the line-item HTML elements along with its existing testing.

The key value is unique to React. For structures like unordered lists, React keeps track of individual elements via their keys, which should be unique. This helps it know if something should be updated or not. In this example, the position of the values within the array are used.

Play with the example on Repl.it!