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.
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, 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: 'table', // ('modal', 'row', 'cell', 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: () => (
<Button
color="blue"
onClick={handleSaveUsers}
disabled={
Object.keys(editedUsers).length === 0 ||
Object.values(validationErrors).some((error) => !!error)
}
loading={isUpdatingUser}
>
Save
</Button>
),
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