MRT logoMantine React Table

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