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 |
---|---|---|---|---|---|
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, 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: 'table', // ('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<Button200color="blue"201onClick={handleSaveUsers}202disabled={203Object.keys(editedUsers).length === 0 ||204Object.values(validationErrors).some((error) => !!error)205}206loading={isUpdatingUser}207>208Save209</Button>210),211renderTopToolbarCustomActions: ({ table }) => (212<Button213onClick={() => {214table.setCreatingRow(true); //simplest way to open the create row modal with no default values215//or you can pass in a row object to set default values with the `createRow` helper function216// table.setCreatingRow(217// createRow(table, {218// //optionally pass in default values for the new row, useful for nested data or other complex scenarios219// }),220// );221}}222>223Create New User224</Button>225),226state: {227isLoading: isLoadingUsers,228isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,229showAlertBanner: isLoadingUsersError,230showProgressBars: isFetchingUsers,231},232});233234return <MantineReactTable table={table} />;235};236237//CREATE hook (post new user to api)238function useCreateUser() {239const queryClient = useQueryClient();240return useMutation({241mutationFn: async (user: User) => {242//send api update request here243await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call244return Promise.resolve();245},246//client side optimistic update247onMutate: (newUserInfo: User) => {248queryClient.setQueryData(249['users'],250(prevUsers: any) =>251[252...prevUsers,253{254...newUserInfo,255id: (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 demo261});262}263264//READ hook (get users from api)265function useGetUsers() {266return useQuery<User[]>({267queryKey: ['users'],268queryFn: async () => {269//send api request here270await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call271return Promise.resolve(fakeData);272},273refetchOnWindowFocus: false,274});275}276277//UPDATE hook (put users in api)278function useUpdateUsers() {279const queryClient = useQueryClient();280return useMutation({281mutationFn: async (users: User[]) => {282//send api update request here283await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call284return Promise.resolve();285},286//client side optimistic update287onMutate: (newUsers: User[]) => {288queryClient.setQueryData(289['users'],290(prevUsers: any) =>291prevUsers?.map((user: User) => {292const newUser = newUsers.find((u) => u.id === user.id);293return newUser ? newUser : user;294}),295);296},297// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo298});299}300301//DELETE hook (delete user in api)302function useDeleteUser() {303const queryClient = useQueryClient();304return useMutation({305mutationFn: async (userId: string) => {306//send api update request here307await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call308return Promise.resolve();309},310//client side optimistic update311onMutate: (userId: string) => {312queryClient.setQueryData(313['users'],314(prevUsers: any) =>315prevUsers?.filter((user: User) => user.id !== userId),316);317},318// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo319});320}321322const queryClient = new QueryClient();323324const ExampleWithProviders = () => (325//Put this with your other react-query providers near root of your app326<QueryClientProvider client={queryClient}>327<ModalsProvider>328<Example />329</ModalsProvider>330</QueryClientProvider>331);332333export default ExampleWithProviders;334335const validateRequired = (value: string) => !!value?.length;336const validateEmail = (email: string) =>337!!email.length &&338339.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);343344function validateUser(user: User) {345return {346firstName: !validateRequired(user.firstName)347? 'First Name is Required'348: '',349lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',350email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',351};352}
1import { useMemo, useState } from 'react';2import {3MantineReactTable,4// createRow,5useMantineReactTable,6} from 'mantine-react-table';7import { ActionIcon, Button, 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: 'table', // ('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<Button192color="blue"193onClick={handleSaveUsers}194disabled={195Object.keys(editedUsers).length === 0 ||196Object.values(validationErrors).some((error) => !!error)197}198loading={isUpdatingUser}199>200Save201</Button>202),203renderTopToolbarCustomActions: ({ table }) => (204<Button205onClick={() => {206table.setCreatingRow(true); //simplest way to open the create row modal with no default values207//or you can pass in a row object to set default values with the `createRow` helper function208// table.setCreatingRow(209// createRow(table, {210// //optionally pass in default values for the new row, useful for nested data or other complex scenarios211// }),212// );213}}214>215Create New User216</Button>217),218state: {219isLoading: isLoadingUsers,220isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,221showAlertBanner: isLoadingUsersError,222showProgressBars: isFetchingUsers,223},224});225226return <MantineReactTable table={table} />;227};228229//CREATE hook (post new user to api)230function useCreateUser() {231const queryClient = useQueryClient();232return useMutation({233mutationFn: async (user) => {234//send api update request here235await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call236return Promise.resolve();237},238//client side optimistic update239onMutate: (newUserInfo) => {240queryClient.setQueryData(['users'], (prevUsers) => [241...prevUsers,242{243...newUserInfo,244id: (Math.random() + 1).toString(36).substring(7),245},246]);247},248// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo249});250}251252//READ hook (get users from api)253function useGetUsers() {254return useQuery({255queryKey: ['users'],256queryFn: async () => {257//send api request here258await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call259return Promise.resolve(fakeData);260},261refetchOnWindowFocus: false,262});263}264265//UPDATE hook (put users in api)266function useUpdateUsers() {267const queryClient = useQueryClient();268return useMutation({269mutationFn: async (users) => {270//send api update request here271await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call272return Promise.resolve();273},274//client side optimistic update275onMutate: (newUsers) => {276queryClient.setQueryData(['users'], (prevUsers) =>277prevUsers?.map((user) => {278const newUser = newUsers.find((u) => u.id === user.id);279return newUser ? newUser : user;280}),281);282},283// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo284});285}286287//DELETE hook (delete user in api)288function useDeleteUser() {289const queryClient = useQueryClient();290return useMutation({291mutationFn: async (userId) => {292//send api update request here293await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call294return Promise.resolve();295},296//client side optimistic update297onMutate: (userId) => {298queryClient.setQueryData(['users'], (prevUsers) =>299prevUsers?.filter((user) => user.id !== userId),300);301},302// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo303});304}305306const queryClient = new QueryClient();307308const ExampleWithProviders = () => (309//Put this with your other react-query providers near root of your app310<QueryClientProvider client={queryClient}>311<ModalsProvider>312<Example />313</ModalsProvider>314</QueryClientProvider>315);316317export default ExampleWithProviders;318319const validateRequired = (value) => !!value?.length;320const validateEmail = (email) =>321!!email.length &&322323.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);327328function validateUser(user) {329return {330firstName: !validateRequired(user.firstName)331? 'First Name is Required'332: '',333lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',334email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',335};336}
View Extra Storybook Examples