Author avatar

Kimaru Thagana

How to Create Dynamic Tables from Editable Columns in React HTML

Kimaru Thagana

  • Aug 10, 2020
  • 16 Min read
  • 764 Views
  • Aug 10, 2020
  • 16 Min read
  • 764 Views
Web Development
Front End Web Development
Client-side Framework
React

Introduction

Tables are critical for displaying data in web apps. They are often complemented by forms on a different page or on a modal. However, at times, creating a form is hectic when you just need to edit just a column. Hence the need to create editable tables to reduce the hassle of having to build a form for the table. An advantage of this approach is that you also get to improve your app's user experience (UX) by minimizing the interfaces a user has to interact with. You will also be able to load data dynamically populating table rows, ensuring your tables can always have updated data.

In this guide, you will learn to build a simple inventory table with the Unit Price column editable to enable updating the price of a product. The table will source its data from a Mock API, which you can create using this guide: Creating a Mock API in React.

This guide assumes that you are familiar with HTML(table element), React Hooks, Creating a Mock API, and making API calls using fetch.

Creating the Mock API

Set up the Mock API as explained in my previous guide, Creating a Mock API in React.

When done, replace the content of the db.json with the code below:

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
{
  "inventory": [
    {
      "id": 1,
      "product_name": "Weetabix",
      "product_category": "Cereal",
      "unit_price": "501",
    },
    {
      "id": 2,
      "product_name": "Colgate Toothpaste",
      "product_category": "Toiletries",
      "unit_price": "119",
    },
    {
      "id": 3,
      "product_name": "Imperial Leather Soap",
      "product_category": "Toiletries",
      "unit_price": "235",
    },
    {
      "id": 4,
      "product_name": "Sunlight Detergent",
      "product_category": "Toiletries",
      "unit_price": "401",
    }
  ]
}
json

To start up your API, run the command below:

1
json-server --watch src/db.json
bash

You should see be able to see your API running with an endpoint, http:/localhost:3000/inventory.

Setting Up the React App

Use Create-React-App, a scaffold that lets you create React apps with no build configurations.

Ensure you have create-react-app installed on your machine. If not, you can install it by running the following:

1
npm install -g create-react-app
bash

Once it is installed, to create the app, run the following:

1
npx create-react-app simple-inventory-table
bash

To start your app, run the command:

1
yarn start
bash

Creating the Table

Open the App.js file and replace the default content with 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
import React from 'react';

function App() {
    return (
        <div className="container">
            <h1>Simple Inventory Table</h1>
            <table>
                <thead>
                <tr>
                    <th>Product Name</th>
                    <th>Product Category</th>
                    <th>Unit Price</th>
                    <th>Action</th>
                </tr>
                </thead>
                <tbody>
                    <tr>
                        <td/>
                        <td/>
                        <td/>
                        <td/>
                    </tr>
                </tbody>
            </table>
        </div>
    );
}

export default App;
jsx

Above, you have defined a div with classname container to house the table. You have defined the title of the page.

Populating the Table

To populate data, make a GET request to the Mock API URL you created. Define the API_HOST of your Mock API and the endpoint. This is done outside the App function:

1
2
3
4
5
6
7
8
import React from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    ...
}
jsx

Use React Hooks to define state and lifecycles in the app. Start by defining a state variable to store the data from the Mock API. The default value is set to an empty array.

1
2
3
4
5
6
7
8
9
10
import React, {useState} from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    // data state variable defaulted to an empty array
    const [data, setData] = useState([]);
    ...
}
jsx

Using fetch, define a function to make a GET request to your Mock API. The setData function is used to update the data state variable on getting a successful response from the Mock API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, {useState} from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    const [data, setData] = useState([]);
    // GET request function to your Mock API
    const fetchInventory = () => {
        fetch(`${INVENTORY_API_URL}`)
            .then(res => res.json())
            .then(json => setData(json));
    }
    ...
}
jsx

Call the function on component mount to get the inventory data to populate the table. To dynamically create the rows, iterate through the data and use the .map() function.

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
import React, {useEffect, useState} from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    const [data, setData] = useState([]);

    // GET request function to your Mock API
    const fetchInventory = () => {
        fetch(`${INVENTORY_API_URL}`)
            .then(res => res.json())
            .then(json => setData(json));
    }
    
    // Calling the function on component mount
    useEffect(() => {
        fetchInventory();
    }, []);


    return (
        <div className="container">
            <h1>Simple Inventory Table</h1>
            <table>
                <thead>
                <tr>
                    <th>Product Name</th>
                    <th>Product Category</th>
                    <th>Unit Price</th>
                    <th>Action</th>
                </tr>
                </thead>
                <tbody>
                    {
                        data.map((item) => (
                            <tr key={item.id}>
                                <td>{item.product_name}</td>
                                <td>{item.product_category}</td>
                                <td>{item.unit_price}</td>
                                <td/>
                            </tr>
                        ))
                    }
                </tbody>
            </table>
        </div>
    );
}

export default App;
jsx

Making the Table Editable

Define a state variable inEditMode to track the edit status. This is an object with two properties:

  • status: A Boolean that shows whether the table is in edit mode or not.
  • rowKey: The ID of the product being edited and indicates which row on the table is being edited.

Define a state variable unitPrice to hold the unit price of the row being edited.

Define a function onEdit that takes in an object as an argument. The object has two properties:

  • id: The ID of the product of the row being edited.
  • currentUnitPrice: The current unit price of the product(row being edited), which is used to set the unitPrice state variable that initializes the input visible on edit mode.

Define a function onCancel, which resets the inEditMode and unitPrice state variables.

Define a function updateInventory that takes in an object as an argument. The object has two properties:

  • id: The ID of the product of the row being edited.
  • newUnitPrice: The updated unit price of the product(row being edited). The function sends a PATCH request to our Mock API, updating it with the new unit price. On successful update, the inEditMode and unitPrice state variables are reset and the updated list of inventory fetched.

Define a function onSave that takes in an object as an argument. The object has two properties:

  • id: The ID of the product of the row being edited.
  • newUnitPrice: The updated unit price of the product(row being edited). The function calls the updateInventory function to update the unit price.
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
74
75
76
import React, {useEffect, useState} from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    ...

    const [inEditMode, setInEditMode] = useState({
        status: false,
        rowKey: null
    });

    const [unitPrice, setUnitPrice] = useState(null);

    /**
     *
     * @param id - The id of the product
     * @param currentUnitPrice - The current unit price of the product
     */
    const onEdit = ({id, currentUnitPrice}) => {
        setInEditMode({
            status: true,
            rowKey: id
        })
        setUnitPrice(currentUnitPrice);
    }

    /**
     *
     * @param id
     * @param newUnitPrice
     */
    const updateInventory = ({id, newUnitPrice}) => {
        fetch(`${INVENTORY_API_URL}/${id}`, {
            method: "PATCH",
            body: JSON.stringify({
                unit_price: newUnitPrice
            }),
            headers: {
                "Content-type": "application/json; charset=UTF-8"
            }
        })
            .then(response => response.json())
            .then(json => {
                // reset inEditMode and unit price state values
                onCancel();

                // fetch the updated data
                fetchInventory();
            })
    }

    /**
     *
     * @param id -The id of the product
     * @param newUnitPrice - The new unit price of the product
     */
    const onSave = ({id, newUnitPrice}) => {
        updateInventory({id, newUnitPrice});
    }

    const onCancel = () => {
        // reset the inEditMode state value
        setInEditMode({
            status: false,
            rowKey: null
        })
        // reset the unit price state value
        setUnitPrice(null);
    }

    ...
}

export default App;
jsx

The final code should look like this:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import React, {useEffect, useState} from 'react';

const API_HOST = "http://localhost:3000";
const INVENTORY_API_URL = `${API_HOST}/inventory`;

function App() {
    const [data, setData] = useState([]);

    const fetchInventory = () => {
        fetch(`${INVENTORY_API_URL}`)
            .then(res => res.json())
            .then(json => setData(json));
    }

    useEffect(() => {
        fetchInventory();
    }, []);


    const [inEditMode, setInEditMode] = useState({
        status: false,
        rowKey: null
    });

    const [unitPrice, setUnitPrice] = useState(null);

    /**
     *
     * @param id - The id of the product
     * @param currentUnitPrice - The current unit price of the product
     */
    const onEdit = ({id, currentUnitPrice}) => {
        setInEditMode({
            status: true,
            rowKey: id
        })
        setUnitPrice(currentUnitPrice);
    }

    /**
     *
     * @param id
     * @param newUnitPrice
     */
    const updateInventory = ({id, newUnitPrice}) => {
        fetch(`${INVENTORY_API_URL}/${id}`, {
            method: "PATCH",
            body: JSON.stringify({
                unit_price: newUnitPrice
            }),
            headers: {
                "Content-type": "application/json; charset=UTF-8"
            }
        })
            .then(response => response.json())
            .then(json => {
                // reset inEditMode and unit price state values
                onCancel();

                // fetch the updated data
                fetchInventory();
            })
    }

    /**
     *
     * @param id -The id of the product
     * @param newUnitPrice - The new unit price of the product
     */
    const onSave = ({id, newUnitPrice}) => {
        updateInventory({id, newUnitPrice});
    }

    const onCancel = () => {
        // reset the inEditMode state value
        setInEditMode({
            status: false,
            rowKey: null
        })
        // reset the unit price state value
        setUnitPrice(null);
    }

    return (
        <div className="container">
            <h1>Simple Inventory Table</h1>
            <table>
                <thead>
                <tr>
                    <th>Product Name</th>
                    <th>Product Category</th>
                    <th>Unit Price</th>
                    <th>Action</th>
                </tr>
                </thead>
                <tbody>
                {
                    data.map((item) => (
                        <tr key={item.id}>
                            <td>{item.product_name}</td>
                            <td>{item.product_category}</td>
                            <td>
                                {
                                    inEditMode.status && inEditMode.rowKey === item.id ? (
                                        <input value={unitPrice}
                                               onChange={(event) => setUnitPrice(event.target.value)}
                                        />
                                    ) : (
                                        item.unit_price
                                    )
                                }
                            </td>
                            <td>
                                {
                                    inEditMode.status && inEditMode.rowKey === item.id ? (
                                        <React.Fragment>
                                            <button
                                                className={"btn-success"}
                                                onClick={() => onSave({id: item.id, newUnitPrice: unitPrice})}
                                            >
                                                Save
                                            </button>

                                            <button
                                                className={"btn-secondary"}
                                                style={{marginLeft: 8}}
                                                onClick={() => onCancel()}
                                            >
                                                Cancel
                                            </button>
                                        </React.Fragment>
                                    ) : (
                                        <button
                                            className={"btn-primary"}
                                            onClick={() => onEdit({id: item.id, currentUnitPrice: item.unit_price})}
                                        >
                                            Edit
                                        </button>
                                    )
                                }
                            </td>
                        </tr>
                    ))
                }
                </tbody>
            </table>
        </div>
    );
}

export default App;
jsx

In the table, you added a ternary operator on the Unit Price and Action columns.

In the Unit Price column, when in edit mode, an input is visible to enable editing of the unit price. Otherwise, the unit price value is visible.

Conclusion

There you have it. A table sourcing data from a Mock API that you used to dynamically create the rows. You also learned how to make a column editable. As a supplement to a previous guide, you made a PATCH request to the Mock API previously created. To build on the knowledge gained in this guide, further explore how to extend your Mock APIs using json-server.

2