Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

How to Create Dynamic Tables from Editable Columns in React HTML

Aug 10, 2020 • 16 Minute Read

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:

      {
  "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",
    }
  ]
}
    

To start up your API, run the command below:

      json-server --watch src/db.json
    

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:

      npm install -g create-react-app
    

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

      npx create-react-app simple-inventory-table
    

To start your app, run the command:

      yarn start
    

Creating the Table

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

      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;
    

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:

      import React from 'react';

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

function App() {
    ...
}
    

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.

      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([]);
    ...
}
    

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.

      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));
    }
    ...
}
    

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.

      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;
    

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.
      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;
    

The final code should look like this:

      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;
    

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.

Learn More

Explore these React courses from Pluralsight to continue learning: