MRT logoMantine React Table

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