Create Dynamic Editable Table using React Hooks

Vanu Protap Verma
4 min readMar 23, 2023

A couple of years ago, I published a 3 part series on how to create a dynamic, editable table using React. Link

In that tutorial, I was using so called “old approach” in my code. In this tutorial, I am wishing to rewrite the code using newer approach and will be using React Hooks.

Since my previous post was very detailed and covered step-by-step changes, here I am sharing the complete code for all of the components I have used in that old post.

If you want to go through that old post to get an overall idea of what we are trying to achieve, please refer to this url and follow the other parts as well.

Updated code for GenericCustomTable component (GenericCustomTable.js file)

import React, { useState, useEffect } from 'react';
import clonedeep from 'lodash.clonedeep';

export default function GenericCustomTable(props) {

const [currentTableData, setCurrentTableData] = useState(props.data || undefined);
const [columnsToDisplay, setColumnsToDisplay] = useState(props.columns || undefined);

useEffect(() => {
setCurrentTableData(props.data);
setColumnsToDisplay(props.columns);
}, [props.data, props.columns])

const renderHeaders = () => {
return columnsToDisplay.map((item, index) => {
const headerCssClassName = `col-md-${item.columnSize}`;
if (item.visible) {
return (
<div className={headerCssClassName} key={index}>
<span className="table-column-header-text">{item.displayText}</span>
</div>
);
} else {
return (
<div className={headerCssClassName} key={index} hidden>
<span className="table-column-header-text">{item.displayText}</span>
</div>
);
}
});
};

const renderIndividualRow = (data, dataKeys) => {
return dataKeys.map((item, index) => {
let columnWidth = `col-md-${columnsToDisplay[index].columnSize}`;
if (item.visible) {
if (item.renderer) {
return (
<div className={columnWidth} key={index}>
{item.renderer(data, item.fieldName)}
</div>
);
} else {
return (
<div className={columnWidth} key={index}>
{data[item.fieldName]}
</div>
);
}
} else {
return null;
}
});
};

const renderRows = () => {
let dataKeys = clonedeep(columnsToDisplay);
let dataRows = clonedeep(currentTableData);
if (dataRows.length > 0) {
return dataRows.map((row, index) => {
return (
<div key={index} className="row">
{renderIndividualRow(row, dataKeys)}
</div>
);
});
}
}

return (
<div className="col-md-12">
<div className="row column-header-row">
{renderHeaders()}
</div>
{renderRows()}
</div>
);
}

and updated code for the Users component (Users.js file)

import React, { useState } from 'react';
import clonedeep from 'lodash.clonedeep';
import GenericCustomTable from './GenericCustomTable';

const initial_users_list = [{
"id": "1",
"first_name": "Shara",
"last_name": "Weeds",
"email": "sweeds0@barnesandnoble.com",
"gender": "Female"
}, {
"id": "2",
"first_name": "Conant",
"last_name": "Puddan",
"email": "cpuddan1@ihg.com",
"gender": "Male"
}, {
"id": "3",
"first_name": "Mehetabel",
"last_name": "Mawtus",
"email": "mmawtus2@sakura.ne.jp",
"gender": "Female"
}
];

export default function Users() {

const [users, setUsers] = useState(clonedeep(initial_users_list));
const [initialUsers, setInitialUsers] = useState(clonedeep(initial_users_list));
const [dataId, setDataId] = useState('');

const emailRenderer = (data, fieldName) => {
const fieldValue = data[fieldName];
const url = `https://www.google.com?search=${fieldValue}`;
return (
<a href={url}>
{fieldValue}
</a>
);
};

const editButtonClickHandler = (data) => {
setDataId(data.id);
};

const cancelButtonClickHandler = () => {
setDataId('');
setUsers(clonedeep(initialUsers));
}

const saveButtonClickHandler = (event) => {
const fieldAndId = event.target.id.split('-');
if (dataId && dataId === fieldAndId[1]) {
const updatedUser = users.find(user => user.id === dataId);
// TODO: call backend service/api to save the data; POST request
console.log(`USER SAVED ${JSON.stringify(updatedUser)}`);
setDataId('');
// below line just gives an impression that the data is saved
setInitialUsers(clonedeep(users));
}
}

const editRenderer = (data, fieldName) => {
if (dataId && dataId === data.id) {
const saveButtonId = `save-${data.id}`;
const cancelButtonId = `cancel-${data.id}`;
return (
<div>
<button
id={saveButtonId}
onClick={saveButtonClickHandler}
>
Save
</button>
<button
id={cancelButtonId}
onClick={cancelButtonClickHandler}
>
Cancel
</button>
</div>
);
}
return (
<div>
<button
onClick={() => editButtonClickHandler(data)}
>
Edit
</button>
</div>
);
}

const firstNameChangeHandler = (event) => {
const fieldAndId = event.target.id.split('-');
const fieldName = fieldAndId[0];
const updatedUsers = clonedeep(users);
updatedUsers.forEach(user => {
if (user.id === fieldAndId[1]) {
user[fieldName] = event.target.value;
}
});
setUsers(clonedeep(updatedUsers));
}

const firstNameRenderer = (data, fieldName) => {
if (dataId && dataId === data.id) {
const inputId = `${fieldName}-${data.id}`;
return (
<div>
<input type="text"
id={inputId}
className="input-textbox"
value={data[fieldName]}
onChange={firstNameChangeHandler}
/>
</div>
);
}
return (
<div>
<span>
{data[fieldName]}
</span>
</div>
);
}

const getDisplayColumns = () => {
return [
{
fieldName: 'id',
displayText: 'ID',
visible: false,
columnSize: 1
},
{
fieldName: 'first_name',
displayText: 'First name',
visible: true,
columnSize: 2,
renderer: firstNameRenderer
},
{
fieldName: 'last_name',
displayText: 'Last name',
visible: true,
columnSize: 2
},
{
fieldName: 'email',
displayText: 'Email',
visible: true,
columnSize: 4,
renderer: emailRenderer
},
{
fieldName: 'gender',
displayText: 'Gender',
visible: true,
columnSize: 2,
},
{
fieldName: 'action_buttons',
displayText: 'Action',
visible: true,
columnSize: 2,
renderer: editRenderer
}
];
};

const displayColumns = getDisplayColumns();
return (
<div className="container">
<div className="row">
<div className="col-md-12">
Rendered Generic Custom Table
</div>
</div>
<br />
<div className="row">
<GenericCustomTable
data={users}
columns={displayColumns}
/>
</div>
<br />
</div>
);

}

The package.json file is like below. Please note that for simplicity, I haven’t gone through updating the packages to their latest versions. If you do update the packages, then you may get some breaking changes and may require to fix those changes before being able to run this code.

{
"name": "react-dynamic-table",
"version": "1.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"bootstrap": "^4.4.1",
"font-awesome": "^4.7.0",
"lodash.clonedeep": "^4.5.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

I hope this post helps you.

--

--