MRT logoMantine React Table

Editing (CRUD) Inline Table 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 "table" editing mode, which allows you to edit a single cell at a time, but all rows are always in an open editing state. 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, 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: 'table', // ('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
<Button
200
color="blue"
201
onClick={handleSaveUsers}
202
disabled={
203
Object.keys(editedUsers).length === 0 ||
204
Object.values(validationErrors).some((error) => !!error)
205
}
206
loading={isUpdatingUser}
207
>
208
Save
209
</Button>
210
),
211
renderTopToolbarCustomActions: ({ table }) => (
212
<Button
213
onClick={() => {
214
table.setCreatingRow(true); //simplest way to open the create row modal with no default values
215
//or you can pass in a row object to set default values with the `createRow` helper function
216
// table.setCreatingRow(
217
// createRow(table, {
218
// //optionally pass in default values for the new row, useful for nested data or other complex scenarios
219
// }),
220
// );
221
}}
222
>
223
Create New User
224
</Button>
225
),
226
state: {
227
isLoading: isLoadingUsers,
228
isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
229
showAlertBanner: isLoadingUsersError,
230
showProgressBars: isFetchingUsers,
231
},
232
});
233
234
return <MantineReactTable table={table} />;
235
};
236
237
//CREATE hook (post new user to api)
238
function useCreateUser() {
239
const queryClient = useQueryClient();
240
return useMutation({
241
mutationFn: async (user: User) => {
242
//send api update request here
243
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
244
return Promise.resolve();
245
},
246
//client side optimistic update
247
onMutate: (newUserInfo: User) => {
248
queryClient.setQueryData(
249
['users'],
250
(prevUsers: any) =>
251
[
252
...prevUsers,
253
{
254
...newUserInfo,
255
id: (Math.random() + 1).toString(36).substring(7),
256
},
257
] as User[],
258
);
259
},
260
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
261
});
262
}
263
264
//READ hook (get users from api)
265
function useGetUsers() {
266
return useQuery<User[]>({
267
queryKey: ['users'],
268
queryFn: async () => {
269
//send api request here
270
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
271
return Promise.resolve(fakeData);
272
},
273
refetchOnWindowFocus: false,
274
});
275
}
276
277
//UPDATE hook (put users in api)
278
function useUpdateUsers() {
279
const queryClient = useQueryClient();
280
return useMutation({
281
mutationFn: async (users: User[]) => {
282
//send api update request here
283
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
284
return Promise.resolve();
285
},
286
//client side optimistic update
287
onMutate: (newUsers: User[]) => {
288
queryClient.setQueryData(
289
['users'],
290
(prevUsers: any) =>
291
prevUsers?.map((user: User) => {
292
const newUser = newUsers.find((u) => u.id === user.id);
293
return newUser ? newUser : user;
294
}),
295
);
296
},
297
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
298
});
299
}
300
301
//DELETE hook (delete user in api)
302
function useDeleteUser() {
303
const queryClient = useQueryClient();
304
return useMutation({
305
mutationFn: async (userId: string) => {
306
//send api update request here
307
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
308
return Promise.resolve();
309
},
310
//client side optimistic update
311
onMutate: (userId: string) => {
312
queryClient.setQueryData(
313
['users'],
314
(prevUsers: any) =>
315
prevUsers?.filter((user: User) => user.id !== userId),
316
);
317
},
318
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
319
});
320
}
321
322
const queryClient = new QueryClient();
323
324
const ExampleWithProviders = () => (
325
//Put this with your other react-query providers near root of your app
326
<QueryClientProvider client={queryClient}>
327
<ModalsProvider>
328
<Example />
329
</ModalsProvider>
330
</QueryClientProvider>
331
);
332
333
export default ExampleWithProviders;
334
335
const validateRequired = (value: string) => !!value?.length;
336
const validateEmail = (email: string) =>
337
!!email.length &&
338
email
339
.toLowerCase()
340
.match(
341
/^(([^<>()[\]\\.,;:\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,}))$/,
342
);
343
344
function validateUser(user: User) {
345
return {
346
firstName: !validateRequired(user.firstName)
347
? 'First Name is Required'
348
: '',
349
lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
350
email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
351
};
352
}
1
import { useMemo, useState } from 'react';
2
import {
3
MantineReactTable,
4
// createRow,
5
useMantineReactTable,
6
} from 'mantine-react-table';
7
import { ActionIcon, Button, 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: 'table', // ('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
<Button
192
color="blue"
193
onClick={handleSaveUsers}
194
disabled={
195
Object.keys(editedUsers).length === 0 ||
196
Object.values(validationErrors).some((error) => !!error)
197
}
198
loading={isUpdatingUser}
199
>
200
Save
201
</Button>
202
),
203
renderTopToolbarCustomActions: ({ table }) => (
204
<Button
205
onClick={() => {
206
table.setCreatingRow(true); //simplest way to open the create row modal with no default values
207
//or you can pass in a row object to set default values with the `createRow` helper function
208
// table.setCreatingRow(
209
// createRow(table, {
210
// //optionally pass in default values for the new row, useful for nested data or other complex scenarios
211
// }),
212
// );
213
}}
214
>
215
Create New User
216
</Button>
217
),
218
state: {
219
isLoading: isLoadingUsers,
220
isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
221
showAlertBanner: isLoadingUsersError,
222
showProgressBars: isFetchingUsers,
223
},
224
});
225
226
return <MantineReactTable table={table} />;
227
};
228
229
//CREATE hook (post new user to api)
230
function useCreateUser() {
231
const queryClient = useQueryClient();
232
return useMutation({
233
mutationFn: async (user) => {
234
//send api update request here
235
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
236
return Promise.resolve();
237
},
238
//client side optimistic update
239
onMutate: (newUserInfo) => {
240
queryClient.setQueryData(['users'], (prevUsers) => [
241
...prevUsers,
242
{
243
...newUserInfo,
244
id: (Math.random() + 1).toString(36).substring(7),
245
},
246
]);
247
},
248
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
249
});
250
}
251
252
//READ hook (get users from api)
253
function useGetUsers() {
254
return useQuery({
255
queryKey: ['users'],
256
queryFn: async () => {
257
//send api request here
258
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
259
return Promise.resolve(fakeData);
260
},
261
refetchOnWindowFocus: false,
262
});
263
}
264
265
//UPDATE hook (put users in api)
266
function useUpdateUsers() {
267
const queryClient = useQueryClient();
268
return useMutation({
269
mutationFn: async (users) => {
270
//send api update request here
271
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
272
return Promise.resolve();
273
},
274
//client side optimistic update
275
onMutate: (newUsers) => {
276
queryClient.setQueryData(['users'], (prevUsers) =>
277
prevUsers?.map((user) => {
278
const newUser = newUsers.find((u) => u.id === user.id);
279
return newUser ? newUser : user;
280
}),
281
);
282
},
283
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
284
});
285
}
286
287
//DELETE hook (delete user in api)
288
function useDeleteUser() {
289
const queryClient = useQueryClient();
290
return useMutation({
291
mutationFn: async (userId) => {
292
//send api update request here
293
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
294
return Promise.resolve();
295
},
296
//client side optimistic update
297
onMutate: (userId) => {
298
queryClient.setQueryData(['users'], (prevUsers) =>
299
prevUsers?.filter((user) => user.id !== userId),
300
);
301
},
302
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
303
});
304
}
305
306
const queryClient = new QueryClient();
307
308
const ExampleWithProviders = () => (
309
//Put this with your other react-query providers near root of your app
310
<QueryClientProvider client={queryClient}>
311
<ModalsProvider>
312
<Example />
313
</ModalsProvider>
314
</QueryClientProvider>
315
);
316
317
export default ExampleWithProviders;
318
319
const validateRequired = (value) => !!value?.length;
320
const validateEmail = (email) =>
321
!!email.length &&
322
email
323
.toLowerCase()
324
.match(
325
/^(([^<>()[\]\\.,;:\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,}))$/,
326
);
327
328
function validateUser(user) {
329
return {
330
firstName: !validateRequired(user.firstName)
331
? 'First Name is Required'
332
: '',
333
lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
334
email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
335
};
336
}
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