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

No records to display

0-0 of 0

Source Code

1import { lazy, Suspense, 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
331//react query setup in App.tsx
332const ReactQueryDevtoolsProduction = lazy(() =>
333 import('@tanstack/react-query-devtools/build/modern/production.js').then(
334 (d) => ({
335 default: d.ReactQueryDevtools,
336 }),
337 ),
338);
339
340const queryClient = new QueryClient();
341
342export default function App() {
343 return (
344 <QueryClientProvider client={queryClient}>
345 <Example />
346 <Suspense fallback={null}>
347 <ReactQueryDevtoolsProduction />
348 </Suspense>
349 </QueryClientProvider>
350 );
351}
352
353const validateRequired = (value: string) => !!value.length;
354const validateEmail = (email: string) =>
355 !!email.length &&
356 email
357 .toLowerCase()
358 .match(
359 /^(([^<>()[\]\\.,;:\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,}))$/,
360 );
361
362function validateUser(user: User) {
363 return {
364 firstName: !validateRequired(user.firstName)
365 ? 'First Name is Required'
366 : '',
367 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
368 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
369 };
370}
371

View Extra Storybook Examples