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.

Check out the other editing modes down below, and the editing guide for more information.

Non TanStack Query Fetching
More Examples

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

View Extra Storybook Examples