Author avatar

Raphael Alampay

Creating Dynamic, Editable Tables with React

Raphael Alampay

  • Sep 25, 2020
  • 9 Min read
  • 25,952 Views
  • Sep 25, 2020
  • 9 Min read
  • 25,952 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:

1import React from 'react';
2
3export default class DynamicTable extends React.Component {
4  constructor(props) {
5    super(props);
6
7    this.state = {
8      message: "",
9      items: []
10    }
11  }
12
13  render() {
14    return (
15      <div>
16        <table>
17          <thead>
18            <tr>
19              <th>Item</th>
20              <th>Actions</th>
21            </tr>
22          </thead>
23          <tbody>
24          </tbody>
25        </table>
26      </div>
27    );
28  }
29}
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:

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

Add in the event handler to update the message:

1updateMessage(event) {
2  this.setState({
3    message: event.target.value
4  });
5}
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:

1handleClick() {
2  var items = this.state.items;
3
4  items.push(this.state.message);
5
6  this.setState({
7    items: items
8  });
9}
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<button onClick={this.handleClick.bind(this)}>
2  Add Item
3</button>
jsx

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

1renderRows() {
2  var context = this;
3
4  return  this.state.items.map(function(o, i) {
5            return (
6              <tr key={"item-" + i}>
7                <td>
8                  <input
9                    type="text"
10                    value={o}
11                  />
12                </td>
13                <td>
14                  <button>
15                    Delete
16                  </button>
17                </td>
18              </tr>
19            );
20          });
21}
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<tbody>
2  {this.renderRows()}
3</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:

1handleItemChanged(i, event) {
2  var items = this.state.items;
3
4  items[i] = event.target.value;
5
6  this.setState({
7    items: items
8  });
9}
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<td>
2  <input
3    type="text"
4    value={o}
5    onChange={context.handleItemChanged.bind(context, i)}
6  />
7</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:

1handleItemDelete(i) {
2  var items = this.state.items;
3
4  items.splice(i, 1);
5
6  this.setState({
7    items: items
8  });
9}
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<td>
2  <button
3    onClick={context.handleItemDelete.bind(context, i)}
4  >
5    Delete
6  </button>
7</td>
jsx

Overall Code

The complete code looks like the following:

1import React from 'react';
2
3export default class DynamicTable extends React.Component {
4  constructor(props) {
5    super(props);
6
7    this.state = {
8      message: "",
9      items: []
10    }
11  }
12
13  updateMessage(event) {
14    this.setState({
15      message: event.target.value
16    });
17  }
18
19  handleClick() {
20    var items = this.state.items;
21
22    items.push(this.state.message);
23
24    this.setState({
25      items: items,
26      message: ""
27    });
28  }
29
30  handleItemChanged(i, event) {
31    var items = this.state.items;
32    items[i]  = event.target.value;
33
34    this.setState({
35      items: items
36    });
37  }
38
39  handleItemDeleted(i) {
40    var items = this.state.items;
41
42    items.splice(i, 1);
43
44    this.setState({
45      items: items
46    });
47  }
48
49  renderRows() {
50    var context = this;
51
52    return  this.state.items.map(function(o, i) {
53              return (
54                <tr key={"item-" + i}>
55                  <td>
56                    <input
57                      type="text"
58                      value={o}
59                      onChange={context.handleItemChanged.bind(context, i)}
60                    />
61                  </td>
62                  <td>
63                    <button
64                      onClick={context.handleItemDeleted.bind(context, i)}
65                    >
66                      Delete
67                    </button>
68                  </td>
69                </tr>
70              );
71            });
72  }
73
74  render() {
75    return (
76      <div>
77        <table className="">
78          <thead>
79            <tr>
80              <th>
81                Item
82              </th>
83              <th>
84                Actions
85              </th>
86            </tr>
87          </thead>
88          <tbody>
89            {this.renderRows()}
90          </tbody>
91        </table>
92        <hr/>
93        <input
94          type="text"
95          value={this.state.message}
96          onChange={this.updateMessage.bind(this)}
97        />
98        <button
99          onClick={this.handleClick.bind(this)}
100        >
101          Add Item
102        </button>
103      </div>
104    );
105  }
106}
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!