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.
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}
To start up your API, run the command below:
1json-server --watch src/db.json
You should see be able to see your API running with an endpoint, http:/localhost:3000/inventory
.
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
Once it is installed, to create the app, run the following:
1npx create-react-app simple-inventory-table
To start your app, run the command:
1yarn start
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;
Above, you have defined a div with classname container to house the table. You have defined the title of the page.
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}
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}
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}
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;
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;
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;
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.
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.