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

1-10 of 10

Source Code

1import { useMemo, useState } from 'react';
2import {
3 MRT_EditActionButtons,
4 MaterialReactTable,
5 // createRow,
6 type MRT_ColumnDef,
7 type MRT_Row,
8 type MRT_TableOptions,
9 useMaterialReactTable,
10} from 'material-react-table';
11import {
12 Box,
13 Button,
14 DialogActions,
15 DialogContent,
16 DialogTitle,
17 IconButton,
18 Stack,
19 Tooltip,
20 Typography,
21} from '@mui/material';
22import {
23 QueryClient,
24 QueryClientProvider,
25 useMutation,
26 useQuery,
27 useQueryClient,
28} from '@tanstack/react-query';
29import { type User, fakeData, usStates } from './makeData';
30import EditIcon from '@mui/icons-material/Edit';
31import DeleteIcon from '@mui/icons-material/Delete';
32
33const Example = () => {
34 const [validationErrors, setValidationErrors] = useState<
35 Record<string, string | undefined>
36 >({});
37
38 const columns = useMemo<MRT_ColumnDef<User>[]>(
39 () => [
40 {
41 accessorKey: 'id',
42 header: 'Id',
43 enableEditing: false,
44 size: 80,
45 },
46 {
47 accessorKey: 'firstName',
48 header: 'First Name',
49 muiEditTextFieldProps: {
50 type: 'email',
51 required: true,
52 error: !!validationErrors?.firstName,
53 helperText: validationErrors?.firstName,
54 //remove any previous validation errors when user focuses on the input
55 onFocus: () =>
56 setValidationErrors({
57 ...validationErrors,
58 firstName: undefined,
59 }),
60 //optionally add validation checking for onBlur or onChange
61 },
62 },
63 {
64 accessorKey: 'lastName',
65 header: 'Last Name',
66 muiEditTextFieldProps: {
67 type: 'email',
68 required: true,
69 error: !!validationErrors?.lastName,
70 helperText: validationErrors?.lastName,
71 //remove any previous validation errors when user focuses on the input
72 onFocus: () =>
73 setValidationErrors({
74 ...validationErrors,
75 lastName: undefined,
76 }),
77 },
78 },
79 {
80 accessorKey: 'email',
81 header: 'Email',
82 muiEditTextFieldProps: {
83 type: 'email',
84 required: true,
85 error: !!validationErrors?.email,
86 helperText: validationErrors?.email,
87 //remove any previous validation errors when user focuses on the input
88 onFocus: () =>
89 setValidationErrors({
90 ...validationErrors,
91 email: undefined,
92 }),
93 },
94 },
95 {
96 accessorKey: 'state',
97 header: 'State',
98 editVariant: 'select',
99 editSelectOptions: usStates,
100 muiEditTextFieldProps: {
101 select: true,
102 error: !!validationErrors?.state,
103 helperText: validationErrors?.state,
104 },
105 },
106 ],
107 [validationErrors],
108 );
109
110 //call CREATE hook
111 const { mutateAsync: createUser, isLoading: isCreatingUser } =
112 useCreateUser();
113 //call READ hook
114 const {
115 data: fetchedUsers = [],
116 isError: isLoadingUsersError,
117 isFetching: isFetchingUsers,
118 isLoading: isLoadingUsers,
119 } = useGetUsers();
120 //call UPDATE hook
121 const { mutateAsync: updateUser, isLoading: isUpdatingUser } =
122 useUpdateUser();
123 //call DELETE hook
124 const { mutateAsync: deleteUser, isLoading: isDeletingUser } =
125 useDeleteUser();
126
127 //CREATE action
128 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
129 values,
130 table,
131 }) => {
132 const newValidationErrors = validateUser(values);
133 if (Object.values(newValidationErrors).some((error) => error)) {
134 setValidationErrors(newValidationErrors);
135 return;
136 }
137 setValidationErrors({});
138 await createUser(values);
139 table.setCreatingRow(null); //exit creating mode
140 };
141
142 //UPDATE action
143 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({
144 values,
145 table,
146 }) => {
147 const newValidationErrors = validateUser(values);
148 if (Object.values(newValidationErrors).some((error) => error)) {
149 setValidationErrors(newValidationErrors);
150 return;
151 }
152 setValidationErrors({});
153 await updateUser(values);
154 table.setEditingRow(null); //exit editing mode
155 };
156
157 //DELETE action
158 const openDeleteConfirmModal = (row: MRT_Row<User>) => {
159 if (window.confirm('Are you sure you want to delete this user?')) {
160 deleteUser(row.original.id);
161 }
162 };
163
164 const table = useMaterialReactTable({
165 columns,
166 data: fetchedUsers,
167 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)
168 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)
169 enableEditing: true,
170 getRowId: (row) => row.id,
171 muiToolbarAlertBannerProps: isLoadingUsersError
172 ? {
173 color: 'error',
174 children: 'Error loading data',
175 }
176 : undefined,
177 muiTableContainerProps: {
178 sx: {
179 minHeight: '500px',
180 },
181 },
182 onCreatingRowCancel: () => setValidationErrors({}),
183 onCreatingRowSave: handleCreateUser,
184 onEditingRowCancel: () => setValidationErrors({}),
185 onEditingRowSave: handleSaveUser,
186 //optionally customize modal content
187 renderCreateRowModalContent: ({ table, row, internalEditComponents }) => (
188 <>
189 <DialogTitle variant="h3">Create New User</DialogTitle>
190 <DialogContent
191 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
192 >
193 {internalEditComponents} {/* or render custom edit components here */}
194 </DialogContent>
195 <DialogActions>
196 <MRT_EditActionButtons variant="text" table={table} row={row} />
197 </DialogActions>
198 </>
199 ),
200 //optionally customize modal content
201 renderEditRowModalContent: ({ table, row, internalEditComponents }) => (
202 <>
203 <DialogTitle variant="h3">Edit User</DialogTitle>
204 <DialogContent
205 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
206 >
207 {internalEditComponents} {/* or render custom edit components here */}
208 </DialogContent>
209 <DialogActions>
210 <MRT_EditActionButtons variant="text" table={table} row={row} />
211 </DialogActions>
212 </>
213 ),
214 renderRowActions: ({ row, table }) => (
215 <Box sx={{ display: 'flex', gap: '1rem' }}>
216 <Tooltip title="Edit">
217 <IconButton onClick={() => table.setEditingRow(row)}>
218 <EditIcon />
219 </IconButton>
220 </Tooltip>
221 <Tooltip title="Delete">
222 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
223 <DeleteIcon />
224 </IconButton>
225 </Tooltip>
226 </Box>
227 ),
228 renderTopToolbarCustomActions: ({ table }) => (
229 <Button
230 variant="contained"
231 onClick={() => {
232 table.setCreatingRow(true); //simplest way to open the create row modal with no default values
233 //or you can pass in a row object to set default values with the `createRow` helper function
234 // table.setCreatingRow(
235 // createRow(table, {
236 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios
237 // }),
238 // );
239 }}
240 >
241 Create New User
242 </Button>
243 ),
244 state: {
245 isLoading: isLoadingUsers,
246 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
247 showAlertBanner: isLoadingUsersError,
248 showProgressBars: isFetchingUsers,
249 },
250 });
251
252 return <MaterialReactTable table={table} />;
253};
254
255//CREATE hook (post new user to api)
256function useCreateUser() {
257 const queryClient = useQueryClient();
258 return useMutation({
259 mutationFn: async (user: User) => {
260 //send api update request here
261 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
262 return Promise.resolve();
263 },
264 //client side optimistic update
265 onMutate: (newUserInfo: User) => {
266 queryClient.setQueryData(
267 ['users'],
268 (prevUsers: any) =>
269 [
270 ...prevUsers,
271 {
272 ...newUserInfo,
273 id: (Math.random() + 1).toString(36).substring(7),
274 },
275 ] as User[],
276 );
277 },
278 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
279 });
280}
281
282//READ hook (get users from api)
283function useGetUsers() {
284 return useQuery<User[]>({
285 queryKey: ['users'],
286 queryFn: async () => {
287 //send api request here
288 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
289 return Promise.resolve(fakeData);
290 },
291 refetchOnWindowFocus: false,
292 });
293}
294
295//UPDATE hook (put user in api)
296function useUpdateUser() {
297 const queryClient = useQueryClient();
298 return useMutation({
299 mutationFn: async (user: User) => {
300 //send api update request here
301 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
302 return Promise.resolve();
303 },
304 //client side optimistic update
305 onMutate: (newUserInfo: User) => {
306 queryClient.setQueryData(
307 ['users'],
308 (prevUsers: any) =>
309 prevUsers?.map((prevUser: User) =>
310 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,
311 ),
312 );
313 },
314 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
315 });
316}
317
318//DELETE hook (delete user in api)
319function useDeleteUser() {
320 const queryClient = useQueryClient();
321 return useMutation({
322 mutationFn: async (userId: string) => {
323 //send api update request here
324 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
325 return Promise.resolve();
326 },
327 //client side optimistic update
328 onMutate: (userId: string) => {
329 queryClient.setQueryData(
330 ['users'],
331 (prevUsers: any) =>
332 prevUsers?.filter((user: User) => user.id !== userId),
333 );
334 },
335 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
336 });
337}
338
339const queryClient = new QueryClient();
340
341const ExampleWithProviders = () => (
342 //Put this with your other react-query providers near root of your app
343 <QueryClientProvider client={queryClient}>
344 <Example />
345 </QueryClientProvider>
346);
347
348export default ExampleWithProviders;
349
350const validateRequired = (value: string) => !!value.length;
351const validateEmail = (email: string) =>
352 !!email.length &&
353 email
354 .toLowerCase()
355 .match(
356 /^(([^<>()[\]\\.,;:\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,}))$/,
357 );
358
359function validateUser(user: User) {
360 return {
361 firstName: !validateRequired(user.firstName)
362 ? 'First Name is Required'
363 : '',
364 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
365 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
366 };
367}
368

View Extra Storybook Examples