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.

This example below uses the default "modal" editing mode, where a dialog opens up to edit 1 row at a time. However be sure to check out the other editing modes and see if they fit your use case better. Other editing modes include "row" (1 row inline at a time), "cell" (1 cell inline at time), and "table" (all cells always editable).


Demo

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

View Extra Storybook Examples