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

View Extra Storybook Examples