MRT logoMaterial React Table

Editing (CRUD) Example

Full CRUD (Create, Read, Update, Delete) functionality can be easily implemented with Material React Table, with a combination of editing, toolbar, and row action features.


Demo

Open Code SandboxOpen on GitHub
9s41rpKelvinLangoshJerod14@hotmail.com19Ohio
08m6rxMollyPurdyHugh.Dach79@hotmail.com37Rhode Island
5ymtrcHenryLynchCamden.Macejkovic@yahoo.com20California
ek5b97GlendaDouglasEric0@yahoo.com38Montana
xxtyddLeoneWilliamsonEricka_Mueller52@yahoo.com19Colorado
wzxj9mMckennaFriesenVeda_Feeney@yahoo.com34New York
21dwtzWymanJastMelvin.Pacocha@yahoo.com23Montana
o8oe4kJanickWillmsDelfina12@gmail.com25Nebraska

Rows per page

1-8 of 8

Source Code

1import React, { FC, useCallback, useMemo, useState } from 'react';
2import MaterialReactTable, {
3 MaterialReactTableProps,
4 MRT_Cell,
5 MRT_ColumnDef,
6 MRT_Row,
7} from 'material-react-table';
8import {
9 Box,
10 Button,
11 Dialog,
12 DialogActions,
13 DialogContent,
14 DialogTitle,
15 IconButton,
16 MenuItem,
17 Stack,
18 TextField,
19 Tooltip,
20} from '@mui/material';
21import { Delete, Edit } from '@mui/icons-material';
22import { data, states } from './makeData';
23
24export type Person = {
25 id: string;
26 firstName: string;
27 lastName: string;
28 email: string;
29 age: number;
30 state: string;
31};
32
33const Example: FC = () => {
34 const [createModalOpen, setCreateModalOpen] = useState(false);
35 const [tableData, setTableData] = useState<Person[]>(() => data);
36 const [validationErrors, setValidationErrors] = useState<{
37 [cellId: string]: string;
38 }>({});
39
40 const handleCreateNewRow = (values: Person) => {
41 tableData.push(values);
42 setTableData([...tableData]);
43 };
44
45 const handleSaveRowEdits: MaterialReactTableProps<Person>['onEditingRowSave'] =
46 async ({ exitEditingMode, row, values }) => {
47 if (!Object.keys(validationErrors).length) {
48 tableData[row.index] = values;
49 //send/receive api updates here, then refetch or update local table data for re-render
50 setTableData([...tableData]);
51 exitEditingMode(); //required to exit editing mode and close modal
52 }
53 };
54
55 const handleDeleteRow = useCallback(
56 (row: MRT_Row<Person>) => {
57 if (
58 !confirm(`Are you sure you want to delete ${row.getValue('firstName')}`)
59 ) {
60 return;
61 }
62 //send api delete request here, then refetch or update local table data for re-render
63 tableData.splice(row.index, 1);
64 setTableData([...tableData]);
65 },
66 [tableData],
67 );
68
69 const getCommonEditTextFieldProps = useCallback(
70 (
71 cell: MRT_Cell<Person>,
72 ): MRT_ColumnDef<Person>['muiTableBodyCellEditTextFieldProps'] => {
73 return {
74 error: !!validationErrors[cell.id],
75 helperText: validationErrors[cell.id],
76 onBlur: (event) => {
77 const isValid =
78 cell.column.id === 'email'
79 ? validateEmail(event.target.value)
80 : cell.column.id === 'age'
81 ? validateAge(+event.target.value)
82 : validateRequired(event.target.value);
83 if (!isValid) {
84 //set validation error for cell if invalid
85 setValidationErrors({
86 ...validationErrors,
87 [cell.id]: `${cell.column.columnDef.header} is required`,
88 });
89 } else {
90 //remove validation error for cell if valid
91 delete validationErrors[cell.id];
92 setValidationErrors({
93 ...validationErrors,
94 });
95 }
96 },
97 };
98 },
99 [validationErrors],
100 );
101
102 const columns = useMemo<MRT_ColumnDef<Person>[]>(
103 () => [
104 {
105 accessorKey: 'id',
106 header: 'ID',
107 enableColumnOrdering: false,
108 enableEditing: false, //disable editing on this column
109 enableSorting: false,
110 size: 80,
111 },
112 {
113 accessorKey: 'firstName',
114 header: 'First Name',
115 size: 140,
116 muiTableBodyCellEditTextFieldProps: ({ cell }) => ({
117 ...getCommonEditTextFieldProps(cell),
118 }),
119 },
120 {
121 accessorKey: 'lastName',
122 header: 'Last Name',
123 size: 140,
124 muiTableBodyCellEditTextFieldProps: ({ cell }) => ({
125 ...getCommonEditTextFieldProps(cell),
126 }),
127 },
128 {
129 accessorKey: 'email',
130 header: 'Email',
131 muiTableBodyCellEditTextFieldProps: ({ cell }) => ({
132 ...getCommonEditTextFieldProps(cell),
133 type: 'email',
134 }),
135 },
136 {
137 accessorKey: 'age',
138 header: 'Age',
139 size: 80,
140 muiTableBodyCellEditTextFieldProps: ({ cell }) => ({
141 ...getCommonEditTextFieldProps(cell),
142 type: 'number',
143 }),
144 },
145 {
146 accessorKey: 'state',
147 header: 'State',
148 muiTableBodyCellEditTextFieldProps: {
149 select: true, //change to select for a dropdown
150 children: states.map((state) => (
151 <MenuItem key={state} value={state}>
152 {state}
153 </MenuItem>
154 )),
155 },
156 },
157 ],
158 [getCommonEditTextFieldProps],
159 );
160
161 return (
162 <>
163 <MaterialReactTable
164 displayColumnDefOptions={{
165 'mrt-row-actions': {
166 muiTableHeadCellProps: {
167 align: 'center',
168 },
169 size: 120,
170 },
171 }}
172 columns={columns}
173 data={tableData}
174 editingMode="modal" //default
175 enableColumnOrdering
176 enableEditing
177 onEditingRowSave={handleSaveRowEdits}
178 renderRowActions={({ row, table }) => (
179 <Box sx={{ display: 'flex', gap: '1rem' }}>
180 <Tooltip arrow placement="left" title="Edit">
181 <IconButton onClick={() => table.setEditingRow(row)}>
182 <Edit />
183 </IconButton>
184 </Tooltip>
185 <Tooltip arrow placement="right" title="Delete">
186 <IconButton color="error" onClick={() => handleDeleteRow(row)}>
187 <Delete />
188 </IconButton>
189 </Tooltip>
190 </Box>
191 )}
192 renderTopToolbarCustomActions={() => (
193 <Button
194 color="secondary"
195 onClick={() => setCreateModalOpen(true)}
196 variant="contained"
197 >
198 Create New Account
199 </Button>
200 )}
201 />
202 <CreateNewAccountModal
203 columns={columns}
204 open={createModalOpen}
205 onClose={() => setCreateModalOpen(false)}
206 onSubmit={handleCreateNewRow}
207 />
208 </>
209 );
210};
211
212//example of creating a mui dialog modal for creating new rows
213export const CreateNewAccountModal: FC<{
214 columns: MRT_ColumnDef<Person>[];
215 onClose: () => void;
216 onSubmit: (values: Person) => void;
217 open: boolean;
218}> = ({ open, columns, onClose, onSubmit }) => {
219 const [values, setValues] = useState<any>(() =>
220 columns.reduce((acc, column) => {
221 acc[column.accessorKey ?? ''] = '';
222 return acc;
223 }, {} as any),
224 );
225
226 const handleSubmit = () => {
227 //put your validation logic here
228 onSubmit(values);
229 onClose();
230 };
231
232 return (
233 <Dialog open={open}>
234 <DialogTitle textAlign="center">Create New Account</DialogTitle>
235 <DialogContent>
236 <form onSubmit={(e) => e.preventDefault()}>
237 <Stack
238 sx={{
239 width: '100%',
240 minWidth: { xs: '300px', sm: '360px', md: '400px' },
241 gap: '1.5rem',
242 }}
243 >
244 {columns.map((column) => (
245 <TextField
246 key={column.accessorKey}
247 label={column.header}
248 name={column.accessorKey}
249 onChange={(e) =>
250 setValues({ ...values, [e.target.name]: e.target.value })
251 }
252 />
253 ))}
254 </Stack>
255 </form>
256 </DialogContent>
257 <DialogActions sx={{ p: '1.25rem' }}>
258 <Button onClick={onClose}>Cancel</Button>
259 <Button color="secondary" onClick={handleSubmit} variant="contained">
260 Create New Account
261 </Button>
262 </DialogActions>
263 </Dialog>
264 );
265};
266
267const validateRequired = (value: string) => !!value.length;
268const validateEmail = (email: string) =>
269 !!email.length &&
270 email
271 .toLowerCase()
272 .match(
273 /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
274 );
275const validateAge = (age: number) => age >= 18 && age <= 50;
276
277export default Example;
278

View Extra Storybook Examples