EN
React - create dynamic editable table
9 points
In this short article, we would like to show how to create simple dynamic editable table in React.

Note: react this article to see standalone editable table cell example.
Practical example:
xxxxxxxxxx
1
// Note: Uncomment import lines in your project.
2
// import React from 'react';
3
// import ReactDOM from 'react-dom';
4
5
// CELL -----------------------------------------
6
7
const itemStyle = {
8
padding: '0px',
9
position: 'relative',
10
height: '26px',
11
display: 'flex',
12
alignItems: 'center'
13
};
14
15
const textStyle = {
16
itemStyle,
17
padding: '0px 4px',
18
height: '26px',
19
fontFamily: 'Arial',
20
fontSize: '13px'
21
};
22
23
const inputStyle = {
24
padding: '0',
25
position: 'absolute',
26
left: '2px',
27
top: '2px',
28
right: '45px',
29
bottom: '2px',
30
minWidth: '20px',
31
fontFamily: 'Arial',
32
fontSize: '13px'
33
};
34
35
const buttonStyle = {
36
position: 'absolute',
37
top: '2px',
38
right: '2px',
39
bottom: '2px',
40
width: '40px'
41
};
42
43
const Cell = React.memo(({value, mode, onChange}) => {
44
const [localMode, setLocalMode] = React.useState(mode ?? 'read');
45
const [localValue, setLocalValue] = React.useState(value ?? '');
46
React.useEffect(() => setLocalMode(mode ?? 'read'), [mode]);
47
React.useEffect(() => setLocalValue(value ?? ''), [value]);
48
if (localMode === 'edit') {
49
const handleInputChange = (e) => setLocalValue(e.target.value);
50
const handleSaveClick = () => {
51
setLocalMode('read');
52
onChange?.(localValue);
53
};
54
return (
55
<div style={itemStyle}>
56
<input type="text"
57
value={localValue}
58
style={inputStyle}
59
onChange={handleInputChange} />
60
<button style={buttonStyle} onClick={handleSaveClick}>Ok</button>
61
</div>
62
);
63
}
64
if (localMode === 'read') {
65
const handleEditClick = () => {
66
setLocalMode('edit');
67
};
68
return (
69
<div style={textStyle} onClick={handleEditClick}>{localValue}</div>
70
);
71
}
72
return null;
73
});
74
75
// ROW ------------------------------------------
76
77
const tdStyle = {
78
padding: '1px',
79
border: '1px solid black',
80
};
81
82
const optionStyle = {
83
tdStyle,
84
padding: '2px 2px',
85
width: '30px'
86
};
87
88
const Row = React.memo(({ mode, columns, data, onChange, onDelete}) => {
89
const handleDeleteClick = () => onDelete?.();
90
return (
91
<tr>
92
{columns.map(({path}, columnIndex) => {
93
const handleChange = value => {
94
if (onChange) {
95
const changedData = { data, [path]: value };
96
onChange(columnIndex, changedData);
97
}
98
};
99
return (
100
<td key={path} style={tdStyle}>
101
<Cell
102
mode={mode}
103
value={data[path]}
104
onChange={handleChange}
105
/>
106
</td>
107
);
108
})}
109
<td style={optionStyle}>
110
<button onClick={handleDeleteClick}>Delete</button>
111
</td>
112
</tr>
113
);
114
});
115
116
// TABLE ----------------------------------------
117
118
const tableStyle = {
119
border: '1px solid black',
120
borderCollapse: 'collapse',
121
width: '100%'
122
}
123
124
const Table = React.memo(({id, columns, data, onAdd, onChange, onDelete}) => {
125
const [addedIndex, setAddedIndex] = React.useState();
126
const handleAddClick = () => {
127
onAdd?.(data.length);
128
setAddedIndex(data.length);
129
};
130
return (
131
<div>
132
<table style={tableStyle}>
133
<tbody>
134
<tr>
135
{columns.map(({path, name}) => (
136
<th key={path} style={tdStyle}>{name}</th>
137
))}
138
</tr>
139
{data.map((rowData, rowIndex) => {
140
const handleChange = (columnIndex, changedData) => {
141
onChange?.(rowIndex, columnIndex, changedData);
142
};
143
const handleDelete = () => {
144
if (rowIndex !== addedIndex) {
145
setAddedIndex(addedIndex - 1);
146
}
147
onDelete?.(rowIndex, rowData);
148
};
149
return (
150
<Row
151
key={rowData[id]}
152
mode={addedIndex === rowIndex ? 'edit' : 'read'}
153
columns={columns}
154
data={rowData}
155
onChange={handleChange}
156
onDelete={handleDelete}
157
/>
158
);
159
})}
160
</tbody>
161
</table>
162
<br />
163
<div>
164
<button onClick={handleAddClick}>Add row</button>
165
</div>
166
</div>
167
);
168
});
169
170
// UTILS ----------------------------------------
171
172
// https://dirask.com/snippets/React-append-prepend-remove-and-replace-items-in-array-with-utils-for-useState-D7XEop
173
174
const appendItem = (updater, item) => {
175
updater(array => array.concat(item));
176
};
177
178
const replaceItem = (updater, index, item) => {
179
updater(array => array.map((value, i) => i === index ? item : value));
180
};
181
182
const deleteItem = (updater, index) => {
183
updater(array => array.filter((value, i) => i !== index));
184
};
185
186
187
188
// Example --------------------------------------
189
190
const columns = [
191
{ path: 'id', name: 'ID' },
192
{ path: 'name', name: 'Name' },
193
{ path: 'age', name: 'Age' }
194
];
195
196
let counter = 0;
197
198
const App = () => {
199
const [data, setData] = React.useState(() => ([
200
{ id: ++counter, name: 'Bob', age: 22 },
201
{ id: ++counter, name: 'Adam', age: 43 },
202
{ id: ++counter, name: 'Mark', age: 16 },
203
{ id: ++counter, name: 'John', age: 29 }
204
]));
205
const handleAdd = (rowIndex) => {
206
const newRowData = { id: ++counter };
207
appendItem(setData, newRowData);
208
//TODO: AJAX request to server
209
console.log(`Added empty row!`);
210
};
211
const handleChange = (rowIndex, columnIndex, changedRowData) => {
212
replaceItem(setData, rowIndex, changedRowData);
213
const changedRowJson = JSON.stringify(changedRowData, null, 4);
214
//TODO: AJAX request to server
215
console.log(`Changed row:\n${changedRowJson}`);
216
};
217
const handleDelete = (rowIndex, deletedRowData) => {
218
deleteItem(setData, rowIndex);
219
//TODO: AJAX request to server
220
console.log(`Deleted row: ${rowIndex}`);
221
};
222
return (
223
<div>
224
<Table
225
id="id"
226
columns={columns}
227
data={data}
228
onAdd={handleAdd}
229
onChange={handleChange}
230
onDelete={handleDelete}
231
/>
232
</div >
233
);
234
};
235
236
const root = document.querySelector('#root');
237
ReactDOM.render(<App/>, root );