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.
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}
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
.
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}
Add in the event handler to update the message:
1updateMessage(event) {
2 this.setState({
3 message: event.target.value
4 });
5}
Bind the event handler to the onChange
attribute of the input:
1<input type="text" onChange={this.updateMessage.bind(this} />
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}
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>
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}
There are two important concepts here:
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.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>
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}
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>
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.
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}
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>
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}
Try it out yourself and see that items can be added, modified, and deleted all within a single component.
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!