Author avatar

Kimaru Thagana

How to Create Dynamic Tables from Editable Columns in React HTML

Kimaru Thagana

  • Aug 10, 2020
  • 16 Min read
  • 52,455 Views
  • Aug 10, 2020
  • 16 Min read
  • 52,455 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  "inventory": [
3    {
4      "id": 1,
5      "product_name": "Weetabix",
6      "product_category": "Cereal",
7      "unit_price": "501",
8    },
9    {
10      "id": 2,
11      "product_name": "Colgate Toothpaste",
12      "product_category": "Toiletries",
13      "unit_price": "119",
14    },
15    {
16      "id": 3,
17      "product_name": "Imperial Leather Soap",
18      "product_category": "Toiletries",
19      "unit_price": "235",
20    },
21    {
22      "id": 4,
23      "product_name": "Sunlight Detergent",
24      "product_category": "Toiletries",
25      "unit_price": "401",
26    }
27  ]
28}
json

To start up your API, run the command below:

1json-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:

1npm install -g create-react-app
bash

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

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

To start your app, run the command:

1yarn start
bash

Creating the Table

Open the App.js file and replace the default content with the following:

1import React from 'react';
2
3function App() {
4    return (
5        <div className="container">
6            <h1>Simple Inventory Table</h1>
7            <table>
8                <thead>
9                <tr>
10                    <th>Product Name</th>
11                    <th>Product Category</th>
12                    <th>Unit Price</th>
13                    <th>Action</th>
14                </tr>
15                </thead>
16                <tbody>
17                    <tr>
18                        <td/>
19                        <td/>
20                        <td/>
21                        <td/>
22                    </tr>
23                </tbody>
24            </table>
25        </div>
26    );
27}
28
29export 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:

1import React from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    ...
8}
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.

1import React, {useState} from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    // data state variable defaulted to an empty array
8    const [data, setData] = useState([]);
9    ...
10}
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.

1import React, {useState} from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    const [data, setData] = useState([]);
8    // GET request function to your Mock API
9    const fetchInventory = () => {
10        fetch(`${INVENTORY_API_URL}`)
11            .then(res => res.json())
12            .then(json => setData(json));
13    }
14    ...
15}
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.

1import React, {useEffect, useState} from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    const [data, setData] = useState([]);
8
9    // GET request function to your Mock API
10    const fetchInventory = () => {
11        fetch(`${INVENTORY_API_URL}`)
12            .then(res => res.json())
13            .then(json => setData(json));
14    }
15    
16    // Calling the function on component mount
17    useEffect(() => {
18        fetchInventory();
19    }, []);
20
21
22    return (
23        <div className="container">
24            <h1>Simple Inventory Table</h1>
25            <table>
26                <thead>
27                <tr>
28                    <th>Product Name</th>
29                    <th>Product Category</th>
30                    <th>Unit Price</th>
31                    <th>Action</th>
32                </tr>
33                </thead>
34                <tbody>
35                    {
36                        data.map((item) => (
37                            <tr key={item.id}>
38                                <td>{item.product_name}</td>
39                                <td>{item.product_category}</td>
40                                <td>{item.unit_price}</td>
41                                <td/>
42                            </tr>
43                        ))
44                    }
45                </tbody>
46            </table>
47        </div>
48    );
49}
50
51export 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.
1import React, {useEffect, useState} from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    ...
8
9    const [inEditMode, setInEditMode] = useState({
10        status: false,
11        rowKey: null
12    });
13
14    const [unitPrice, setUnitPrice] = useState(null);
15
16    /**
17     *
18     * @param id - The id of the product
19     * @param currentUnitPrice - The current unit price of the product
20     */
21    const onEdit = ({id, currentUnitPrice}) => {
22        setInEditMode({
23            status: true,
24            rowKey: id
25        })
26        setUnitPrice(currentUnitPrice);
27    }
28
29    /**
30     *
31     * @param id
32     * @param newUnitPrice
33     */
34    const updateInventory = ({id, newUnitPrice}) => {
35        fetch(`${INVENTORY_API_URL}/${id}`, {
36            method: "PATCH",
37            body: JSON.stringify({
38                unit_price: newUnitPrice
39            }),
40            headers: {
41                "Content-type": "application/json; charset=UTF-8"
42            }
43        })
44            .then(response => response.json())
45            .then(json => {
46                // reset inEditMode and unit price state values
47                onCancel();
48
49                // fetch the updated data
50                fetchInventory();
51            })
52    }
53
54    /**
55     *
56     * @param id -The id of the product
57     * @param newUnitPrice - The new unit price of the product
58     */
59    const onSave = ({id, newUnitPrice}) => {
60        updateInventory({id, newUnitPrice});
61    }
62
63    const onCancel = () => {
64        // reset the inEditMode state value
65        setInEditMode({
66            status: false,
67            rowKey: null
68        })
69        // reset the unit price state value
70        setUnitPrice(null);
71    }
72
73    ...
74}
75
76export default App;
jsx

The final code should look like this:

1import React, {useEffect, useState} from 'react';
2
3const API_HOST = "http://localhost:3000";
4const INVENTORY_API_URL = `${API_HOST}/inventory`;
5
6function App() {
7    const [data, setData] = useState([]);
8
9    const fetchInventory = () => {
10        fetch(`${INVENTORY_API_URL}`)
11            .then(res => res.json())
12            .then(json => setData(json));
13    }
14
15    useEffect(() => {
16        fetchInventory();
17    }, []);
18
19
20    const [inEditMode, setInEditMode] = useState({
21        status: false,
22        rowKey: null
23    });
24
25    const [unitPrice, setUnitPrice] = useState(null);
26
27    /**
28     *
29     * @param id - The id of the product
30     * @param currentUnitPrice - The current unit price of the product
31     */
32    const onEdit = ({id, currentUnitPrice}) => {
33        setInEditMode({
34            status: true,
35            rowKey: id
36        })
37        setUnitPrice(currentUnitPrice);
38    }
39
40    /**
41     *
42     * @param id
43     * @param newUnitPrice
44     */
45    const updateInventory = ({id, newUnitPrice}) => {
46        fetch(`${INVENTORY_API_URL}/${id}`, {
47            method: "PATCH",
48            body: JSON.stringify({
49                unit_price: newUnitPrice
50            }),
51            headers: {
52                "Content-type": "application/json; charset=UTF-8"
53            }
54        })
55            .then(response => response.json())
56            .then(json => {
57                // reset inEditMode and unit price state values
58                onCancel();
59
60                // fetch the updated data
61                fetchInventory();
62            })
63    }
64
65    /**
66     *
67     * @param id -The id of the product
68     * @param newUnitPrice - The new unit price of the product
69     */
70    const onSave = ({id, newUnitPrice}) => {
71        updateInventory({id, newUnitPrice});
72    }
73
74    const onCancel = () => {
75        // reset the inEditMode state value
76        setInEditMode({
77            status: false,
78            rowKey: null
79        })
80        // reset the unit price state value
81        setUnitPrice(null);
82    }
83
84    return (
85        <div className="container">
86            <h1>Simple Inventory Table</h1>
87            <table>
88                <thead>
89                <tr>
90                    <th>Product Name</th>
91                    <th>Product Category</th>
92                    <th>Unit Price</th>
93                    <th>Action</th>
94                </tr>
95                </thead>
96                <tbody>
97                {
98                    data.map((item) => (
99                        <tr key={item.id}>
100                            <td>{item.product_name}</td>
101                            <td>{item.product_category}</td>
102                            <td>
103                                {
104                                    inEditMode.status && inEditMode.rowKey === item.id ? (
105                                        <input value={unitPrice}
106                                               onChange={(event) => setUnitPrice(event.target.value)}
107                                        />
108                                    ) : (
109                                        item.unit_price
110                                    )
111                                }
112                            </td>
113                            <td>
114                                {
115                                    inEditMode.status && inEditMode.rowKey === item.id ? (
116                                        <React.Fragment>
117                                            <button
118                                                className={"btn-success"}
119                                                onClick={() => onSave({id: item.id, newUnitPrice: unitPrice})}
120                                            >
121                                                Save
122                                            </button>
123
124                                            <button
125                                                className={"btn-secondary"}
126                                                style={{marginLeft: 8}}
127                                                onClick={() => onCancel()}
128                                            >
129                                                Cancel
130                                            </button>
131                                        </React.Fragment>
132                                    ) : (
133                                        <button
134                                            className={"btn-primary"}
135                                            onClick={() => onEdit({id: item.id, currentUnitPrice: item.unit_price})}
136                                        >
137                                            Edit
138                                        </button>
139                                    )
140                                }
141                            </td>
142                        </tr>
143                    ))
144                }
145                </tbody>
146            </table>
147        </div>
148    );
149}
150
151export 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.