Creating Dynamic, Editable Tables with React
React.js allows you to build complex forms and maintain the values of objects bound to the forms. This guide covers different approaches to building such forms.
Sep 25, 2020 • 9 Minute Read
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:
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>
);
}
}
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:
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>
);
}
Add in the event handler to update the message:
updateMessage(event) {
this.setState({
message: event.target.value
});
}
Bind the event handler to the onChange attribute of the input:
<input type="text" onChange={this.updateMessage.bind(this} />
Next, create the event handler for the button when it is clicked:
handleClick() {
var items = this.state.items;
items.push(this.state.message);
this.setState({
items: items
});
}
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:
<button onClick={this.handleClick.bind(this)}>
Add Item
</button>
To render the items in the table, create a separate function that returns rendered JSX:
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>
);
});
}
There are two important concepts here:
- 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.
- 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:
<tbody>
{this.renderRows()}
</tbody>
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:
handleItemChanged(i, event) {
var items = this.state.items;
items[i] = event.target.value;
this.setState({
items: items
});
}
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:
<td>
<input
type="text"
value={o}
onChange={context.handleItemChanged.bind(context, i)}
/>
</td>
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:
handleItemDelete(i) {
var items = this.state.items;
items.splice(i, 1);
this.setState({
items: items
});
}
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:
<td>
<button
onClick={context.handleItemDelete.bind(context, i)}
>
Delete
</button>
</td>
Overall Code
The complete code looks like the following:
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>
);
}
}
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!