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

View Extra Storybook Examples