MRT logoMantine React Table

Editing (CRUD) Example

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