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.
Id | First Name | Last Name | Email | State | Actions |
---|---|---|---|---|---|
Rows per page
1-10 of 10
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css'; //if using mantine date picker features
import 'mantine-react-table/styles.css'; //make sure MRT styles were imported in your app root (once)
import { useMemo, useState } from 'react';
import {
MantineReactTable,
// createRow,
type MRT_ColumnDef,
type MRT_Row,
type MRT_TableOptions,
useMantineReactTable,
} from 'mantine-react-table';
import { ActionIcon, Button, Flex, Text, Tooltip } from '@mantine/core';
import { ModalsProvider, modals } from '@mantine/modals';
import { IconTrash } from '@tabler/icons-react';
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { type User, fakeData, usStates } from './makeData';
const Example = () => {
const [validationErrors, setValidationErrors] = useState<
Record<string, string | undefined>
>({});
//keep track of rows that have been edited
const [editedUsers, setEditedUsers] = useState<Record<string, User>>({});
//call CREATE hook
const { mutateAsync: createUser, isPending: isCreatingUser } =
useCreateUser();
//call READ hook
const {
data: fetchedUsers = [],
isError: isLoadingUsersError,
isFetching: isFetchingUsers,
isLoading: isLoadingUsers,
} = useGetUsers();
//call UPDATE hook
const { mutateAsync: updateUsers, isPending: isUpdatingUser } =
useUpdateUsers();
//call DELETE hook
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
useDeleteUser();
//CREATE action
const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
values,
exitCreatingMode,
}) => {
const newValidationErrors = validateUser(values);
if (Object.values(newValidationErrors).some((error) => !!error)) {
setValidationErrors(newValidationErrors);
return;
}
setValidationErrors({});
await createUser(values);
exitCreatingMode();
};
//UPDATE action
const handleSaveUsers = async () => {
if (Object.values(validationErrors).some((error) => !!error)) return;
await updateUsers(Object.values(editedUsers));
setEditedUsers({});
};
//DELETE action
const openDeleteConfirmModal = (row: MRT_Row<User>) =>
modals.openConfirmModal({
title: 'Are you sure you want to delete this user?',
children: (
<Text>
Are you sure you want to delete {row.original.firstName}{' '}
{row.original.lastName}? This action cannot be undone.
</Text>
),
labels: { confirm: 'Delete', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => deleteUser(row.original.id),
});
const columns = useMemo<MRT_ColumnDef<User>[]>(
() => [
{
accessorKey: 'id',
header: 'Id',
enableEditing: false,
size: 80,
},
{
accessorKey: 'firstName',
header: 'First Name',
mantineEditTextInputProps: ({ cell, row }) => ({
type: 'text',
required: true,
error: validationErrors?.[cell.id],
//store edited user in state to be saved later
onBlur: (event) => {
const validationError = !validateRequired(event.currentTarget.value)
? 'Required'
: undefined;
setValidationErrors({
...validationErrors,
[cell.id]: validationError,
});
setEditedUsers({ ...editedUsers, [row.id]: row.original });
},
}),
},
{
accessorKey: 'lastName',
header: 'Last Name',
mantineEditTextInputProps: ({ cell, row }) => ({
type: 'text',
required: true,
error: validationErrors?.[cell.id],
//store edited user in state to be saved later
onBlur: (event) => {
const validationError = !validateRequired(event.currentTarget.value)
? 'Required'
: undefined;
setValidationErrors({
...validationErrors,
[cell.id]: validationError,
});
setEditedUsers({ ...editedUsers, [row.id]: row.original });
},
}),
},
{
accessorKey: 'email',
header: 'Email',
mantineEditTextInputProps: ({ cell, row }) => ({
type: 'email',
required: true,
error: validationErrors?.[cell.id],
//store edited user in state to be saved later
onBlur: (event) => {
const validationError = !validateEmail(event.currentTarget.value)
? 'Invalid Email'
: undefined;
setValidationErrors({
...validationErrors,
[cell.id]: validationError,
});
setEditedUsers({ ...editedUsers, [row.id]: row.original });
},
}),
},
{
accessorKey: 'state',
header: 'State',
editVariant: 'select',
mantineEditSelectProps: ({ row }) => ({
data: usStates,
//store edited user in state to be saved later
onChange: (value: any) =>
setEditedUsers({
...editedUsers,
[row.id]: { ...row.original, state: value },
}),
}),
},
],
[editedUsers, validationErrors],
);
const table = useMantineReactTable({
columns,
data: fetchedUsers,
createDisplayMode: 'row', // ('modal', and 'custom' are also available)
editDisplayMode: 'cell', // ('modal', 'row', 'table', and 'custom' are also available)
enableEditing: true,
enableRowActions: true,
positionActionsColumn: 'last',
getRowId: (row) => row.id,
mantineToolbarAlertBannerProps: isLoadingUsersError
? {
color: 'red',
children: 'Error loading data',
}
: undefined,
mantineTableContainerProps: {
style: {
minHeight: '500px',
},
},
onCreatingRowCancel: () => setValidationErrors({}),
onCreatingRowSave: handleCreateUser,
renderRowActions: ({ row }) => (
<Tooltip label="Delete">
<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>
<IconTrash />
</ActionIcon>
</Tooltip>
),
renderBottomToolbarCustomActions: () => (
<Flex align="center" gap="md">
<Button
color="blue"
onClick={handleSaveUsers}
disabled={
Object.keys(editedUsers).length === 0 ||
Object.values(validationErrors).some((error) => !!error)
}
loading={isUpdatingUser}
>
Save
</Button>
{Object.values(validationErrors).some((error) => !!error) && (
<Text color="red">Fix errors before submitting</Text>
)}
</Flex>
),
renderTopToolbarCustomActions: ({ table }) => (
<Button
onClick={() => {
table.setCreatingRow(true); //simplest way to open the create row modal with no default values
//or you can pass in a row object to set default values with the `createRow` helper function
// table.setCreatingRow(
// createRow(table, {
// //optionally pass in default values for the new row, useful for nested data or other complex scenarios
// }),
// );
}}
>
Create New User
</Button>
),
state: {
isLoading: isLoadingUsers,
isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
showAlertBanner: isLoadingUsersError,
showProgressBars: isFetchingUsers,
},
});
return <MantineReactTable table={table} />;
};
//CREATE hook (post new user to api)
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (user: User) => {
//send api update request here
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
return Promise.resolve();
},
//client side optimistic update
onMutate: (newUserInfo: User) => {
queryClient.setQueryData(
['users'],
(prevUsers: any) =>
[
...prevUsers,
{
...newUserInfo,
id: (Math.random() + 1).toString(36).substring(7),
},
] as User[],
);
},
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
});
}
//READ hook (get users from api)
function useGetUsers() {
return useQuery<User[]>({
queryKey: ['users'],
queryFn: async () => {
//send api request here
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
return Promise.resolve(fakeData);
},
refetchOnWindowFocus: false,
});
}
//UPDATE hook (put users in api)
function useUpdateUsers() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (users: User[]) => {
//send api update request here
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
return Promise.resolve();
},
//client side optimistic update
onMutate: (newUsers: User[]) => {
queryClient.setQueryData(['users'], (prevUsers: any) =>
prevUsers?.map((user: User) => {
const newUser = newUsers.find((u) => u.id === user.id);
return newUser ? newUser : user;
}),
);
},
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
});
}
//DELETE hook (delete user in api)
function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userId: string) => {
//send api update request here
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
return Promise.resolve();
},
//client side optimistic update
onMutate: (userId: string) => {
queryClient.setQueryData(['users'], (prevUsers: any) =>
prevUsers?.filter((user: User) => user.id !== userId),
);
},
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
});
}
const queryClient = new QueryClient();
const ExampleWithProviders = () => (
//Put this with your other react-query providers near root of your app
<QueryClientProvider client={queryClient}>
<ModalsProvider>
<Example />
</ModalsProvider>
</QueryClientProvider>
);
export default ExampleWithProviders;
const validateRequired = (value: string) => !!value?.length;
const validateEmail = (email: string) =>
!!email.length &&
email
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\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,}))$/,
);
function validateUser(user: User) {
return {
firstName: !validateRequired(user.firstName)
? 'First Name is Required'
: '',
lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
};
}
View Extra Storybook Examples