Author avatar

Raphael Alampay

Represent Nested Objects in a JSON Array with React.js Components

Raphael Alampay

  • Oct 10, 2020
  • 9 Min read
  • 126 Views
  • Oct 10, 2020
  • 9 Min read
  • 126 Views
Web Development
Front End Web Development
Client-side Frameworks
React

Introduction

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.

Setup

Start off by creating the parent component TodoList:

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

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

    this.state = {
      items: []
    }
  }

  render() {
    return (
      <div>
        <h1>Todo List</h1>
        <hr/>
        <button>
          Add Item
        </button>
      </div>
    );
  }
}
javascript

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:

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
import React from 'react';

export defualt class TodoItem extends React.Component {
  constructor(props) {
    super(props);

    this.state = {}
  }

  updateItem(event) {
  }

  handleClick() {
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.props.item}
          onChange={this.updateItem.bind(this)}
        />
        <button onClick={this.handleClick.bind(this)}>
          Delete
        </button>
      </div>
    );
  }
}
javascript

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.

Logic for Item Management

Next, add the necessary methods within TodoList that deals with adding and deleting an item.

Adding an Item

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

  items.push("");

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

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
2
3
4
5
<button
  onClick={this.handleAddClicked.bind(this)}
>
  Add Item
</button>
jsx

Deleting an Item

Next, create a method for deleting an item:

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

  items.splice(index, 1);

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

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.

Updating an Item

Next, create a method for updating an item:

1
2
3
4
5
6
7
8
9
updateItem(item, index) {
  var items = this.state.items;

  items[index] = item;

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

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.

Passing Functionality to TodoItem

Before you attempt to load TodoItems for each element of items array, make sure to import the component accordingly:

1
import TodoItem from './TodoItem';
javascript

Create a function that returns a mapped set of TodoItems within TodoList called renderItems:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
renderItems() {
  var context = this;

  return (
    this.state.items.map(function(o, i) {
      return  <TodoItem
                key={"item-" + i}
                item={o}
                index={i}
                updateItem={context.updateItem.bind(context)}
                deleteItem={context.deleteItem.bind(context)}
              />
    })
  );
}
javascript

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:

  1. key: The unique identifier for this component
  2. item: The actual element per loop in the array
  3. index: The index of the element
  4. updateItem: A reference to TodoList's updateItem method that can be invoked by TodoItem
  5. 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()}
jsx

Logic Methods for TodoItem

Finally, add some functionality to your TodoItem component for updating and deleting an item:

1
2
3
updateItem(event) {
  this.props.updateItem(event.target.value, this.props.index);
}
javascript

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.

1
2
3
handleClick() {
  this.props.deleteItem(this.props.index);
}
javascript

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
2
3
4
5
6
7
8
<input
  type="text"
  value={this.props.item}
  onChange={this.updateItem.bind(this)}
/>
<button onClick={this.handleClick.bind(this)}>
  Delete
</button>
jsx

Overall Code

TodoItem

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
import React from 'react';

export defualt class TodoItem extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      item: props.item
    }
  }

  updateItem(event) {
    this.props.updateItem(event.target.value, this.props.index);
  }

  handleClick() {
    this.props.deleteItem(this.props.index);
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.props.item}
          onChange={this.updateItem.bind(this)}
        />
        <button onClick={this.handleClick.bind(this)}>
          Delete
        </button>
      </div>
    );
  }
}
javascript

TodoList

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
import React from 'react';
import TodoItem from './TodoItem';

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

    this.state = {
      items: []
    }
  }

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

    items.push("");

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

  updateItem(item, index) {
    var items = this.state.items;

    items[index] = item;

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

  deleteItem(index) {
    var items = this.state.items;

    items.splice(index, 1);

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

  renderItems() {
    var context = this;

    return (
      this.state.items.map(function(o, i) {
        return  <TodoItem
                  key={"item-" + i}
                  item={o}
                  item={i}
                  updateItem={context.updateItem.bind(context)}
                  deleteItem={context.deleteItem.bind(context)}
                />
      })
    );
  }

  render() {
    return (
      <div>
        <h1>Todo List</h1>
        {this.renderItems()}
        <hr/>
        <button
          onClick={this.handleAddClicked.bind(this)}
        >
          Add Item
        </button>
      </div>
    );
  }
}
javascript

Conclusion

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!

0