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

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