Author avatar

Raphael Alampay

Creating Dynamic, Editable Tables with React

Raphael Alampay

  • Sep 25, 2020
  • 9 Min read
  • 137 Views
  • Sep 25, 2020
  • 9 Min read
  • 137 Views
Web Development
Front End Web Development
Client-side Frameworks
React

Introduction

React.js allows you to build complex forms while maintaining the values of the objects bound to the forms. An example of such complexity is maintaining an array of values that can be modified by the user all in a single interface. We can express this by creating a table where each row contains an input element corresponding to the value of an element in the array of an object we want to maintain.

This guide will show you the different approaches to building such a form and how this relates to state management in React.js.

Setup

You'll be creating a single component called DynamicTable that maintains two state attributes: a message and an array of messages called items. Code in the following to get you started:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';

export default class DynamicTable extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "",
      items: []
    }
  }

  render() {
    return (
      <div>
        <table>
          <thead>
            <tr>
              <th>Item</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
          </tbody>
        </table>
      </div>
    );
  }
}
javascript

Notice that the component uses the complete form of an HTML table. React.js complains if you don't have the tbody when you insert rows within table.

Adding Items

Create a simple interface that will allow the user to input a message and a button to submit that message. The idea is that if the user clicks the button, it will take the value of the message and add it to the items array. As the user changes the value in the input, the message state will be updated. The UI will lie just below the table, so your render() method will look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render() {
  return (
    <div>
      <table>
        <thead>
          <tr>
            <th>Item</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
      <hr/>
      <input type="text" />
      <button>
        Add Item
      </button>
    </div>
  );
}
javascript

Add in the event handler to update the message:

1
2
3
4
5
updateMessage(event) {
  this.setState({
    message: event.target.value
  });
}
javascript

Bind the event handler to the onChange attribute of the input:

1
<input type="text" onChange={this.updateMessage.bind(this} />
jsx

Next, create the event handler for the button when it is clicked:

1
2
3
4
5
6
7
8
9
handleClick() {
  var items = this.state.items;

  items.push(this.state.message);

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

All the function is doing is taking the current array of items and the current value of message and pushes them to the array before updating the state.

Bind the event handler to the onClick attribute of the button:

1
2
3
<button onClick={this.handleClick.bind(this)}>
  Add Item
</button>
jsx

To render the items in the table, create a separate function that returns rendered JSX:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
renderRows() {
  var context = this;

  return  this.state.items.map(function(o, i) {
            return (
              <tr key={"item-" + i}>
                <td>
                  <input
                    type="text"
                    value={o}
                  />
                </td>
                <td>
                  <button>
                    Delete
                  </button>
                </td>
              </tr>
            );
          });
}
javascript

There are two important concepts here:

  1. Since items is a dynamic array that's expected to grow or shrink at any time, the function that maps these values to individual <tr> tags should maintain a key attribute for each node it produces. It is a requirement for React.js that the value of its key should be unique within the parent element (in this case, <tbody>). Thus, the value "item-" + i is used where i is the index of an element in the mapped array.
  2. A separate context variable is used to reference this since within the nested returns, you'll need to refer to the instance of the DynamicTable component to bind event handlers to input and button later on.

Invoke renderRows() within the body of the table as follows:

1
2
3
<tbody>
  {this.renderRows()}
</tbody>
jsx

Modifying Items

To modify each element in items within the input of each table row, you'll have to create an event handler that knows which index in the array should be updated. Create a function that looks like this:

1
2
3
4
5
6
7
8
9
handleItemChanged(i, event) {
  var items = this.state.items;

  items[i] = event.target.value;

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

Notice that the first argument to the function is i, corresponding to the index of the array. The second argument is event, which has a property target referring to the input element at hand. You can then update the element at index i of items by assigning it event.target.value.

Hook it in the table row's input element's onChange attribute:

1
2
3
4
5
6
7
<td>
  <input
    type="text"
    value={o}
    onChange={context.handleItemChanged.bind(context, i)}
  />
</td>
jsx

As seen in the code, context is used to call handleItemChanged since it is a reference to this, which is a reference to the component itself. Next, context is bound to the function from bind() as the first argument. Everything after context becomes an argument to the function. In this case you pass i, which is the index as given by the mapping function.

Deleting Items

You can use the same technique to delete items by creating a single event handler for each button generated:

1
2
3
4
5
6
7
8
9
handleItemDelete(i) {
  var items = this.state.items;

  items.splice(i, 1);

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

The method takes in the index i and uses it as an argument to splice(index, x), which removes x items from the array at starting index index. In this case, you just want to remove 1 item, which is the item itself at index i.

Attach it to the button's onClick attribute binding index i as well:

1
2
3
4
5
6
7
<td>
  <button
    onClick={context.handleItemDelete.bind(context, i)}
  >
    Delete
  </button>
</td>
jsx

Overall Code

The complete code looks like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import React from 'react';

export default class DynamicTable extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "",
      items: []
    }
  }

  updateMessage(event) {
    this.setState({
      message: event.target.value
    });
  }

  handleClick() {
    var items = this.state.items;

    items.push(this.state.message);

    this.setState({
      items: items,
      message: ""
    });
  }

  handleItemChanged(i, event) {
    var items = this.state.items;
    items[i]  = event.target.value;

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

  handleItemDeleted(i) {
    var items = this.state.items;

    items.splice(i, 1);

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

  renderRows() {
    var context = this;

    return  this.state.items.map(function(o, i) {
              return (
                <tr key={"item-" + i}>
                  <td>
                    <input
                      type="text"
                      value={o}
                      onChange={context.handleItemChanged.bind(context, i)}
                    />
                  </td>
                  <td>
                    <button
                      onClick={context.handleItemDeleted.bind(context, i)}
                    >
                      Delete
                    </button>
                  </td>
                </tr>
              );
            });
  }

  render() {
    return (
      <div>
        <table className="">
          <thead>
            <tr>
              <th>
                Item
              </th>
              <th>
                Actions
              </th>
            </tr>
          </thead>
          <tbody>
            {this.renderRows()}
          </tbody>
        </table>
        <hr/>
        <input
          type="text"
          value={this.state.message}
          onChange={this.updateMessage.bind(this)}
        />
        <button
          onClick={this.handleClick.bind(this)}
        >
          Add Item
        </button>
      </div>
    );
  }
}
javascript

Try it out yourself and see that items can be added, modified, and deleted all within a single component.

Conclusion

In this guide, child elements are created dynamically and are dependent on the state value of an array maintained by the component. Each element of the array can be modified directly with its own corresponding interface, which is automatically bound to the state by passing the index value to the event handler. As a challenge, try to work with an array of objects instead of an array of strings in order to create more complex nested array bound elements!

1