When dealing with an array of items in a React.js component, it is best practice to create an independent component that handles each element of the array. In this guide, we'll take a look at the classic todo app in React.js but give it a twist by creating a separate TodoItem
component whose responsibility is to handle the functionality for each element in the array. This means that each element of the array will correspond to a TodoItem
component that will be mounted within TodoList
.
Start off by creating the parent component TodoList
:
1import React from 'react';
2
3export default class TodoList extends React.Component {
4 constructor(props) {
5 super(props);
6
7 this.state = {
8 items: []
9 }
10 }
11
12 render() {
13 return (
14 <div>
15 <h1>Todo List</h1>
16 <hr/>
17 <button>
18 Add Item
19 </button>
20 </div>
21 );
22 }
23}
Notice that the component maintains an array of items
in its state. To simplify things, the items
array will just contain string elements.
Next, create the TodoItem
component that will serve as a container for each element of items
. Initially, it would look like the following:
1import React from 'react';
2
3export defualt class TodoItem extends React.Component {
4 constructor(props) {
5 super(props);
6
7 this.state = {}
8 }
9
10 updateItem(event) {
11 }
12
13 handleClick() {
14 }
15
16 render() {
17 return (
18 <div>
19 <input
20 type="text"
21 value={this.props.item}
22 onChange={this.updateItem.bind(this)}
23 />
24 <button onClick={this.handleClick.bind(this)}>
25 Delete
26 </button>
27 </div>
28 );
29 }
30}
You don't need to maintain any state here. The component has its own input element whose value is an item
passed by its props
. Whenever the item
is modified, it invokes the updateItem
method. It also maintains its own delete button that invokes a handleClick
method when clicked.
Next, add the necessary methods within TodoList
that deals with adding and deleting an item.
1handleAddClicked() {
2 var items = this.state.items;
3
4 items.push("");
5
6 this.setState({
7 items: items
8 });
9}
This method is pretty straightforward. It extracts the current items
array and pushes in a blank string before updating the state of items
. Bind the method to the button for adding an item like so:
1<button
2 onClick={this.handleAddClicked.bind(this)}
3>
4 Add Item
5</button>
Next, create a method for deleting an item:
1deleteItem(index) {
2 var items = this.state.items;
3
4 items.splice(index, 1);
5
6 this.setState({
7 items: items
8 });
9}
Since there is no unique identifier for an item
element, you will make use of index information to know which element of the array to delete. This is done by passing it to the splice
method of an array and having 1
as the second argument, making it remove a single item based on the index. The approach is similar to the previous method. Extract the current items
state, remove based on index, and update the items
state. Notice also that you do not call this method anywhere explicitly within TodoList
. Instead, this will be passed as a reference method to each TodoItem
later on.
Next, create a method for updating an item:
1updateItem(item, index) {
2 var items = this.state.items;
3
4 items[index] = item;
5
6 this.setState({
7 items: items
8 });
9}
The logic follows the same pattern as delete. The difference is now you have the newly updated item
as the first argument to be loaded to the array of items
based on index
.
Before you attempt to load TodoItem
s for each element of items
array, make sure to import the component accordingly:
1import TodoItem from './TodoItem';
Create a function that returns a mapped set of TodoItem
s within TodoList
called renderItems
:
1renderItems() {
2 var context = this;
3
4 return (
5 this.state.items.map(function(o, i) {
6 return <TodoItem
7 key={"item-" + i}
8 item={o}
9 index={i}
10 updateItem={context.updateItem.bind(context)}
11 deleteItem={context.deleteItem.bind(context)}
12 />
13 })
14 );
15}
Notice that the first thing you do is to create a reference to this
using a variable context
. This is useful when you want to refer to the instance of TodoList
within deeply nested JavaScript code, such as the one presented in return
. The trick of this method is to iterate through items
and map it to an instance of TodoItem
by passing in the following:
key
: The unique identifier for this componentitem
: The actual element per loop in the arrayindex
: The index of the elementupdateItem
: A reference to TodoList
's updateItem
method that can be invoked by TodoItem
deleteItem
: A reference to TodoList
's deleteItem
method that can be invoked by TodoItem
All of these are accessible by each TodoItem
's props
.
To render the components, invoke renderItems
within the render
method of TodoList
:
1{this.renderItems()}
Finally, add some functionality to your TodoItem
component for updating and deleting an item
:
1updateItem(event) {
2 this.props.updateItem(event.target.value, this.props.index);
3}
This method invokes the updateItem
of its parent via props
. Notice that the value for the first argument (the newly updated item
) is taken from the input element of this TodoItem
. The second argument will be the index
passed to it from props
as well.
1handleClick() {
2 this.props.deleteItem(this.props.index);
3}
Delete works the same way. Invoke the deleteItem
of the parent by accessing it from this TodoItem
's props
. Pass in the index
to be deleted, which is also accessible via props
.
Hook up the logic to TodoItem
's interface:
1<input
2 type="text"
3 value={this.props.item}
4 onChange={this.updateItem.bind(this)}
5/>
6<button onClick={this.handleClick.bind(this)}>
7 Delete
8</button>
1import React from 'react';
2
3export defualt class TodoItem extends React.Component {
4 constructor(props) {
5 super(props);
6
7 this.state = {
8 item: props.item
9 }
10 }
11
12 updateItem(event) {
13 this.props.updateItem(event.target.value, this.props.index);
14 }
15
16 handleClick() {
17 this.props.deleteItem(this.props.index);
18 }
19
20 render() {
21 return (
22 <div>
23 <input
24 type="text"
25 value={this.props.item}
26 onChange={this.updateItem.bind(this)}
27 />
28 <button onClick={this.handleClick.bind(this)}>
29 Delete
30 </button>
31 </div>
32 );
33 }
34}
1import React from 'react';
2import TodoItem from './TodoItem';
3
4export default class TodoList extends React.Component {
5 constructor(props) {
6 super(props);
7
8 this.state = {
9 items: []
10 }
11 }
12
13 handleAddClicked() {
14 var items = this.state.items;
15
16 items.push("");
17
18 this.setState({
19 items: items
20 });
21 }
22
23 updateItem(item, index) {
24 var items = this.state.items;
25
26 items[index] = item;
27
28 this.setState({
29 items: items
30 });
31 }
32
33 deleteItem(index) {
34 var items = this.state.items;
35
36 items.splice(index, 1);
37
38 this.setState({
39 items: items
40 });
41 }
42
43 renderItems() {
44 var context = this;
45
46 return (
47 this.state.items.map(function(o, i) {
48 return <TodoItem
49 key={"item-" + i}
50 item={o}
51 item={i}
52 updateItem={context.updateItem.bind(context)}
53 deleteItem={context.deleteItem.bind(context)}
54 />
55 })
56 );
57 }
58
59 render() {
60 return (
61 <div>
62 <h1>Todo List</h1>
63 {this.renderItems()}
64 <hr/>
65 <button
66 onClick={this.handleAddClicked.bind(this)}
67 >
68 Add Item
69 </button>
70 </div>
71 );
72 }
73}
This may be a slightly more advanced todo app, but it allows you as a developer to be more modular with the way the app handles and modifies information. In fact, it's considered good practice to create separate components for managing individual pieces of information such as elements of an array. Although we simply modified string elements in this example, this can be extended to have nested components deal with more complex objects.
For any questions or concerns, or if you simply want to chat about programming in general, hit me up @happyalampay!