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 |
---|---|---|---|---|---|
1-10 of 10
1import { useMemo, useState } from 'react';2import {3MantineReactTable,4// createRow,5type MRT_ColumnDef,6type MRT_Row,7type MRT_TableOptions,8useMantineReactTable,9} from 'mantine-react-table';10import { ActionIcon, Button, Flex, Text, Tooltip } from '@mantine/core';11import { ModalsProvider, modals } from '@mantine/modals';12import { IconTrash } from '@tabler/icons-react';13import {14QueryClient,15QueryClientProvider,16useMutation,17useQuery,18useQueryClient,19} from '@tanstack/react-query';20import { type User, fakeData, usStates } from './makeData';2122const Example = () => {23const [validationErrors, setValidationErrors] = useState<24Record<string, string | undefined>25>({});26//keep track of rows that have been edited27const [editedUsers, setEditedUsers] = useState<Record<string, User>>({});2829//call CREATE hook30const { mutateAsync: createUser, isLoading: isCreatingUser } =31useCreateUser();32//call READ hook33const {34data: fetchedUsers = [],35isError: isLoadingUsersError,36isFetching: isFetchingUsers,37isLoading: isLoadingUsers,38} = useGetUsers();39//call UPDATE hook40const { mutateAsync: updateUsers, isLoading: isUpdatingUser } =41useUpdateUsers();42//call DELETE hook43const { mutateAsync: deleteUser, isLoading: isDeletingUser } =44useDeleteUser();4546//CREATE action47const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({48values,49exitCreatingMode,50}) => {51const newValidationErrors = validateUser(values);52if (Object.values(newValidationErrors).some((error) => !!error)) {53setValidationErrors(newValidationErrors);54return;55}56setValidationErrors({});57await createUser(values);58exitCreatingMode();59};6061//UPDATE action62const handleSaveUsers = async () => {63if (Object.values(validationErrors).some((error) => !!error)) return;64await updateUsers(Object.values(editedUsers));65setEditedUsers({});66};6768//DELETE action69const openDeleteConfirmModal = (row: MRT_Row<User>) =>70modals.openConfirmModal({71title: 'Are you sure you want to delete this user?',72children: (73<Text>74Are you sure you want to delete {row.original.firstName}{' '}75{row.original.lastName}? This action cannot be undone.76</Text>77),78labels: { confirm: 'Delete', cancel: 'Cancel' },79confirmProps: { color: 'red' },80onConfirm: () => deleteUser(row.original.id),81});8283const columns = useMemo<MRT_ColumnDef<User>[]>(84() => [85{86accessorKey: 'id',87header: 'Id',88enableEditing: false,89size: 80,90},91{92accessorKey: 'firstName',93header: 'First Name',94mantineEditTextInputProps: ({ cell, row }) => ({95type: 'email',96required: true,97error: validationErrors?.[cell.id],98//store edited user in state to be saved later99onBlur: (event) => {100const validationError = !validateRequired(event.currentTarget.value)101? 'Required'102: undefined;103setValidationErrors({104...validationErrors,105[cell.id]: validationError,106});107setEditedUsers({ ...editedUsers, [row.id]: row.original });108},109}),110},111{112accessorKey: 'lastName',113header: 'Last Name',114mantineEditTextInputProps: ({ cell, row }) => ({115type: 'email',116required: true,117error: validationErrors?.[cell.id],118//store edited user in state to be saved later119onBlur: (event) => {120const validationError = !validateRequired(event.currentTarget.value)121? 'Required'122: undefined;123setValidationErrors({124...validationErrors,125[cell.id]: validationError,126});127setEditedUsers({ ...editedUsers, [row.id]: row.original });128},129}),130},131{132accessorKey: 'email',133header: 'Email',134mantineEditTextInputProps: ({ cell, row }) => ({135type: 'email',136required: true,137error: validationErrors?.[cell.id],138//store edited user in state to be saved later139onBlur: (event) => {140const validationError = !validateEmail(event.currentTarget.value)141? 'Invalid Email'142: undefined;143setValidationErrors({144...validationErrors,145[cell.id]: validationError,146});147setEditedUsers({ ...editedUsers, [row.id]: row.original });148},149}),150},151{152accessorKey: 'state',153header: 'State',154editVariant: 'select',155mantineEditSelectProps: ({ row }) => ({156data: usStates,157//store edited user in state to be saved later158onChange: (value: any) =>159setEditedUsers({160...editedUsers,161[row.id]: { ...row.original, state: value },162}),163}),164},165],166[editedUsers, validationErrors],167);168169const table = useMantineReactTable({170columns,171data: fetchedUsers,172createDisplayMode: 'row', // ('modal', and 'custom' are also available)173editDisplayMode: 'cell', // ('modal', 'row', 'cell', and 'custom' are also available)174enableEditing: true,175enableRowActions: true,176positionActionsColumn: 'last',177getRowId: (row) => row.id,178mantineToolbarAlertBannerProps: isLoadingUsersError179? {180color: 'red',181children: 'Error loading data',182}183: undefined,184mantineTableContainerProps: {185sx: {186minHeight: '500px',187},188},189onCreatingRowCancel: () => setValidationErrors({}),190onCreatingRowSave: handleCreateUser,191renderRowActions: ({ row }) => (192<Tooltip label="Delete">193<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>194<IconTrash />195</ActionIcon>196</Tooltip>197),198renderBottomToolbarCustomActions: () => (199<Flex align="center" gap="md">200<Button201color="blue"202onClick={handleSaveUsers}203disabled={204Object.keys(editedUsers).length === 0 ||205Object.values(validationErrors).some((error) => !!error)206}207loading={isUpdatingUser}208>209Save210</Button>211{Object.values(validationErrors).some((error) => !!error) && (212<Text color="red">Fix errors before submitting</Text>213)}214</Flex>215),216renderTopToolbarCustomActions: ({ table }) => (217<Button218onClick={() => {219table.setCreatingRow(true); //simplest way to open the create row modal with no default values220//or you can pass in a row object to set default values with the `createRow` helper function221// table.setCreatingRow(222// createRow(table, {223// //optionally pass in default values for the new row, useful for nested data or other complex scenarios224// }),225// );226}}227>228Create New User229</Button>230),231state: {232isLoading: isLoadingUsers,233isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,234showAlertBanner: isLoadingUsersError,235showProgressBars: isFetchingUsers,236},237});238239return <MantineReactTable table={table} />;240};241242//CREATE hook (post new user to api)243function useCreateUser() {244const queryClient = useQueryClient();245return useMutation({246mutationFn: async (user: User) => {247//send api update request here248await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call249return Promise.resolve();250},251//client side optimistic update252onMutate: (newUserInfo: User) => {253queryClient.setQueryData(254['users'],255(prevUsers: any) =>256[257...prevUsers,258{259...newUserInfo,260id: (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 demo266});267}268269//READ hook (get users from api)270function useGetUsers() {271return useQuery<User[]>({272queryKey: ['users'],273queryFn: async () => {274//send api request here275await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call276return Promise.resolve(fakeData);277},278refetchOnWindowFocus: false,279});280}281282//UPDATE hook (put users in api)283function useUpdateUsers() {284const queryClient = useQueryClient();285return useMutation({286mutationFn: async (users: User[]) => {287//send api update request here288await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call289return Promise.resolve();290},291//client side optimistic update292onMutate: (newUsers: User[]) => {293queryClient.setQueryData(294['users'],295(prevUsers: any) =>296prevUsers?.map((user: User) => {297const newUser = newUsers.find((u) => u.id === user.id);298return newUser ? newUser : user;299}),300);301},302// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo303});304}305306//DELETE hook (delete user in api)307function useDeleteUser() {308const queryClient = useQueryClient();309return useMutation({310mutationFn: async (userId: string) => {311//send api update request here312await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call313return Promise.resolve();314},315//client side optimistic update316onMutate: (userId: string) => {317queryClient.setQueryData(318['users'],319(prevUsers: any) =>320prevUsers?.filter((user: User) => user.id !== userId),321);322},323// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo324});325}326327const queryClient = new QueryClient();328329const ExampleWithProviders = () => (330//Put this with your other react-query providers near root of your app331<QueryClientProvider client={queryClient}>332<ModalsProvider>333<Example />334</ModalsProvider>335</QueryClientProvider>336);337338export default ExampleWithProviders;339340const validateRequired = (value: string) => !!value?.length;341const validateEmail = (email: string) =>342!!email.length &&343344.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);348349function validateUser(user: User) {350return {351firstName: !validateRequired(user.firstName)352? 'First Name is Required'353: '',354lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',355email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',356};357}
1import { useMemo, useState } from 'react';2import {3MantineReactTable,4// createRow,5useMantineReactTable,6} from 'mantine-react-table';7import { ActionIcon, Button, Flex, Text, Tooltip } from '@mantine/core';8import { ModalsProvider, modals } from '@mantine/modals';9import { IconTrash } from '@tabler/icons-react';10import {11QueryClient,12QueryClientProvider,13useMutation,14useQuery,15useQueryClient,16} from '@tanstack/react-query';17import { fakeData, usStates } from './makeData';1819const Example = () => {20const [validationErrors, setValidationErrors] = useState({});21//keep track of rows that have been edited22const [editedUsers, setEditedUsers] = useState({});2324//call CREATE hook25const { mutateAsync: createUser, isLoading: isCreatingUser } =26useCreateUser();27//call READ hook28const {29data: fetchedUsers = [],30isError: isLoadingUsersError,31isFetching: isFetchingUsers,32isLoading: isLoadingUsers,33} = useGetUsers();34//call UPDATE hook35const { mutateAsync: updateUsers, isLoading: isUpdatingUser } =36useUpdateUsers();37//call DELETE hook38const { mutateAsync: deleteUser, isLoading: isDeletingUser } =39useDeleteUser();4041//CREATE action42const handleCreateUser = async ({ values, exitCreatingMode }) => {43const newValidationErrors = validateUser(values);44if (Object.values(newValidationErrors).some((error) => !!error)) {45setValidationErrors(newValidationErrors);46return;47}48setValidationErrors({});49await createUser(values);50exitCreatingMode();51};5253//UPDATE action54const handleSaveUsers = async () => {55if (Object.values(validationErrors).some((error) => !!error)) return;56await updateUsers(Object.values(editedUsers));57setEditedUsers({});58};5960//DELETE action61const openDeleteConfirmModal = (row) =>62modals.openConfirmModal({63title: 'Are you sure you want to delete this user?',64children: (65<Text>66Are you sure you want to delete {row.original.firstName}{' '}67{row.original.lastName}? This action cannot be undone.68</Text>69),70labels: { confirm: 'Delete', cancel: 'Cancel' },71confirmProps: { color: 'red' },72onConfirm: () => deleteUser(row.original.id),73});7475const columns = useMemo(76() => [77{78accessorKey: 'id',79header: 'Id',80enableEditing: false,81size: 80,82},83{84accessorKey: 'firstName',85header: 'First Name',86mantineEditTextInputProps: ({ cell, row }) => ({87type: 'email',88required: true,89error: validationErrors?.[cell.id],90//store edited user in state to be saved later91onBlur: (event) => {92const validationError = !validateRequired(event.currentTarget.value)93? 'Required'94: undefined;95setValidationErrors({96...validationErrors,97[cell.id]: validationError,98});99setEditedUsers({ ...editedUsers, [row.id]: row.original });100},101}),102},103{104accessorKey: 'lastName',105header: 'Last Name',106mantineEditTextInputProps: ({ cell, row }) => ({107type: 'email',108required: true,109error: validationErrors?.[cell.id],110//store edited user in state to be saved later111onBlur: (event) => {112const validationError = !validateRequired(event.currentTarget.value)113? 'Required'114: undefined;115setValidationErrors({116...validationErrors,117[cell.id]: validationError,118});119setEditedUsers({ ...editedUsers, [row.id]: row.original });120},121}),122},123{124accessorKey: 'email',125header: 'Email',126mantineEditTextInputProps: ({ cell, row }) => ({127type: 'email',128required: true,129error: validationErrors?.[cell.id],130//store edited user in state to be saved later131onBlur: (event) => {132const validationError = !validateEmail(event.currentTarget.value)133? 'Invalid Email'134: undefined;135setValidationErrors({136...validationErrors,137[cell.id]: validationError,138});139setEditedUsers({ ...editedUsers, [row.id]: row.original });140},141}),142},143{144accessorKey: 'state',145header: 'State',146editVariant: 'select',147mantineEditSelectProps: ({ row }) => ({148data: usStates,149//store edited user in state to be saved later150onChange: (value) =>151setEditedUsers({152...editedUsers,153[row.id]: { ...row.original, state: value },154}),155}),156},157],158[editedUsers, validationErrors],159);160161const table = useMantineReactTable({162columns,163data: fetchedUsers,164createDisplayMode: 'row', // ('modal', and 'custom' are also available)165editDisplayMode: 'cell', // ('modal', 'row', 'cell', and 'custom' are also available)166enableEditing: true,167enableRowActions: true,168positionActionsColumn: 'last',169getRowId: (row) => row.id,170mantineToolbarAlertBannerProps: isLoadingUsersError171? {172color: 'red',173children: 'Error loading data',174}175: undefined,176mantineTableContainerProps: {177sx: {178minHeight: '500px',179},180},181onCreatingRowCancel: () => setValidationErrors({}),182onCreatingRowSave: handleCreateUser,183renderRowActions: ({ row }) => (184<Tooltip label="Delete">185<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>186<IconTrash />187</ActionIcon>188</Tooltip>189),190renderBottomToolbarCustomActions: () => (191<Flex align="center" gap="md">192<Button193color="blue"194onClick={handleSaveUsers}195disabled={196Object.keys(editedUsers).length === 0 ||197Object.values(validationErrors).some((error) => !!error)198}199loading={isUpdatingUser}200>201Save202</Button>203{Object.values(validationErrors).some((error) => !!error) && (204<Text color="red">Fix errors before submitting</Text>205)}206</Flex>207),208renderTopToolbarCustomActions: ({ table }) => (209<Button210onClick={() => {211table.setCreatingRow(true); //simplest way to open the create row modal with no default values212//or you can pass in a row object to set default values with the `createRow` helper function213// table.setCreatingRow(214// createRow(table, {215// //optionally pass in default values for the new row, useful for nested data or other complex scenarios216// }),217// );218}}219>220Create New User221</Button>222),223state: {224isLoading: isLoadingUsers,225isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,226showAlertBanner: isLoadingUsersError,227showProgressBars: isFetchingUsers,228},229});230231return <MantineReactTable table={table} />;232};233234//CREATE hook (post new user to api)235function useCreateUser() {236const queryClient = useQueryClient();237return useMutation({238mutationFn: async (user) => {239//send api update request here240await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call241return Promise.resolve();242},243//client side optimistic update244onMutate: (newUserInfo) => {245queryClient.setQueryData(['users'], (prevUsers) => [246...prevUsers,247{248...newUserInfo,249id: (Math.random() + 1).toString(36).substring(7),250},251]);252},253// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo254});255}256257//READ hook (get users from api)258function useGetUsers() {259return useQuery({260queryKey: ['users'],261queryFn: async () => {262//send api request here263await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call264return Promise.resolve(fakeData);265},266refetchOnWindowFocus: false,267});268}269270//UPDATE hook (put users in api)271function useUpdateUsers() {272const queryClient = useQueryClient();273return useMutation({274mutationFn: async (users) => {275//send api update request here276await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call277return Promise.resolve();278},279//client side optimistic update280onMutate: (newUsers) => {281queryClient.setQueryData(['users'], (prevUsers) =>282prevUsers?.map((user) => {283const newUser = newUsers.find((u) => u.id === user.id);284return newUser ? newUser : user;285}),286);287},288// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo289});290}291292//DELETE hook (delete user in api)293function useDeleteUser() {294const queryClient = useQueryClient();295return useMutation({296mutationFn: async (userId) => {297//send api update request here298await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call299return Promise.resolve();300},301//client side optimistic update302onMutate: (userId) => {303queryClient.setQueryData(['users'], (prevUsers) =>304prevUsers?.filter((user) => user.id !== userId),305);306},307// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo308});309}310311const queryClient = new QueryClient();312313const ExampleWithProviders = () => (314//Put this with your other react-query providers near root of your app315<QueryClientProvider client={queryClient}>316<ModalsProvider>317<Example />318</ModalsProvider>319</QueryClientProvider>320);321322export default ExampleWithProviders;323324const validateRequired = (value) => !!value?.length;325const validateEmail = (email) =>326!!email.length &&327328.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);332333function validateUser(user) {334return {335firstName: !validateRequired(user.firstName)336? 'First Name is Required'337: '',338lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',339email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',340};341}
View Extra Storybook Examples