Editing (CRUD) Inline Row 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
"row"
editing mode, which allows you to edit a single row at a time with built-in save and cancel buttons.Check out the other editing modes down below, and the editing guide for more information.
Actions | Id | First Name | Last Name | Email | State |
---|---|---|---|---|---|
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 { IconEdit, 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>({});2627const columns = useMemo<MRT_ColumnDef<User>[]>(28() => [29{30accessorKey: 'id',31header: 'Id',32enableEditing: false,33size: 80,34},35{36accessorKey: 'firstName',37header: 'First Name',38mantineEditTextInputProps: {39type: 'email',40required: true,41error: validationErrors?.firstName,42//remove any previous validation errors when user focuses on the input43onFocus: () =>44setValidationErrors({45...validationErrors,46firstName: undefined,47}),48//optionally add validation checking for onBlur or onChange49},50},51{52accessorKey: 'lastName',53header: 'Last Name',54mantineEditTextInputProps: {55type: 'email',56required: true,57error: validationErrors?.lastName,58//remove any previous validation errors when user focuses on the input59onFocus: () =>60setValidationErrors({61...validationErrors,62lastName: undefined,63}),64},65},66{67accessorKey: 'email',68header: 'Email',69mantineEditTextInputProps: {70type: 'email',71required: true,72error: validationErrors?.email,73//remove any previous validation errors when user focuses on the input74onFocus: () =>75setValidationErrors({76...validationErrors,77email: undefined,78}),79},80},81{82accessorKey: 'state',83header: 'State',84editVariant: 'select',85mantineEditSelectProps: {86data: usStates,87error: validationErrors?.state,88},89},90],91[validationErrors],92);9394//call CREATE hook95const { mutateAsync: createUser, isLoading: isCreatingUser } =96useCreateUser();97//call READ hook98const {99data: fetchedUsers = [],100isError: isLoadingUsersError,101isFetching: isFetchingUsers,102isLoading: isLoadingUsers,103} = useGetUsers();104//call UPDATE hook105const { mutateAsync: updateUser, isLoading: isUpdatingUser } =106useUpdateUser();107//call DELETE hook108const { mutateAsync: deleteUser, isLoading: isDeletingUser } =109useDeleteUser();110111//CREATE action112const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({113values,114exitCreatingMode,115}) => {116const newValidationErrors = validateUser(values);117if (Object.values(newValidationErrors).some((error) => error)) {118setValidationErrors(newValidationErrors);119return;120}121setValidationErrors({});122await createUser(values);123exitCreatingMode();124};125126//UPDATE action127const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({128values,129table,130}) => {131const newValidationErrors = validateUser(values);132if (Object.values(newValidationErrors).some((error) => error)) {133setValidationErrors(newValidationErrors);134return;135}136setValidationErrors({});137await updateUser(values);138table.setEditingRow(null); //exit editing mode139};140141//DELETE action142const openDeleteConfirmModal = (row: MRT_Row<User>) =>143modals.openConfirmModal({144title: 'Are you sure you want to delete this user?',145children: (146<Text>147Are you sure you want to delete {row.original.firstName}{' '}148{row.original.lastName}? This action cannot be undone.149</Text>150),151labels: { confirm: 'Delete', cancel: 'Cancel' },152confirmProps: { color: 'red' },153onConfirm: () => deleteUser(row.original.id),154});155156const table = useMantineReactTable({157columns,158data: fetchedUsers,159createDisplayMode: 'row', // ('modal', and 'custom' are also available)160editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)161enableEditing: true,162getRowId: (row) => row.id,163mantineToolbarAlertBannerProps: isLoadingUsersError164? {165color: 'red',166children: 'Error loading data',167}168: undefined,169mantineTableContainerProps: {170sx: {171minHeight: '500px',172},173},174onCreatingRowCancel: () => setValidationErrors({}),175onCreatingRowSave: handleCreateUser,176onEditingRowCancel: () => setValidationErrors({}),177onEditingRowSave: handleSaveUser,178renderRowActions: ({ row, table }) => (179<Flex gap="md">180<Tooltip label="Edit">181<ActionIcon onClick={() => table.setEditingRow(row)}>182<IconEdit />183</ActionIcon>184</Tooltip>185<Tooltip label="Delete">186<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>187<IconTrash />188</ActionIcon>189</Tooltip>190</Flex>191),192renderTopToolbarCustomActions: ({ table }) => (193<Button194onClick={() => {195table.setCreatingRow(true); //simplest way to open the create row modal with no default values196//or you can pass in a row object to set default values with the `createRow` helper function197// table.setCreatingRow(198// createRow(table, {199// //optionally pass in default values for the new row, useful for nested data or other complex scenarios200// }),201// );202}}203>204Create New User205</Button>206),207state: {208isLoading: isLoadingUsers,209isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,210showAlertBanner: isLoadingUsersError,211showProgressBars: isFetchingUsers,212},213});214215return <MantineReactTable table={table} />;216};217218//CREATE hook (post new user to api)219function useCreateUser() {220const queryClient = useQueryClient();221return useMutation({222mutationFn: async (user: User) => {223//send api update request here224await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call225return Promise.resolve();226},227//client side optimistic update228onMutate: (newUserInfo: User) => {229queryClient.setQueryData(230['users'],231(prevUsers: any) =>232[233...prevUsers,234{235...newUserInfo,236id: (Math.random() + 1).toString(36).substring(7),237},238] as User[],239);240},241// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo242});243}244245//READ hook (get users from api)246function useGetUsers() {247return useQuery<User[]>({248queryKey: ['users'],249queryFn: async () => {250//send api request here251await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call252return Promise.resolve(fakeData);253},254refetchOnWindowFocus: false,255});256}257258//UPDATE hook (put user in api)259function useUpdateUser() {260const queryClient = useQueryClient();261return useMutation({262mutationFn: async (user: User) => {263//send api update request here264await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call265return Promise.resolve();266},267//client side optimistic update268onMutate: (newUserInfo: User) => {269queryClient.setQueryData(270['users'],271(prevUsers: any) =>272prevUsers?.map((prevUser: User) =>273prevUser.id === newUserInfo.id ? newUserInfo : prevUser,274),275);276},277// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo278});279}280281//DELETE hook (delete user in api)282function useDeleteUser() {283const queryClient = useQueryClient();284return useMutation({285mutationFn: async (userId: string) => {286//send api update request here287await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call288return Promise.resolve();289},290//client side optimistic update291onMutate: (userId: string) => {292queryClient.setQueryData(293['users'],294(prevUsers: any) =>295prevUsers?.filter((user: User) => user.id !== userId),296);297},298// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo299});300}301302const queryClient = new QueryClient();303304const ExampleWithProviders = () => (305//Put this with your other react-query providers near root of your app306<QueryClientProvider client={queryClient}>307<ModalsProvider>308<Example />309</ModalsProvider>310</QueryClientProvider>311);312313export default ExampleWithProviders;314315const validateRequired = (value: string) => !!value.length;316const validateEmail = (email: string) =>317!!email.length &&318319.toLowerCase()320.match(321/^(([^<>()[\]\\.,;:\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,}))$/,322);323324function validateUser(user: User) {325return {326firstName: !validateRequired(user.firstName)327? 'First Name is Required'328: '',329lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',330email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',331};332}
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 { IconEdit, 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({});2122const columns = useMemo(23() => [24{25accessorKey: 'id',26header: 'Id',27enableEditing: false,28size: 80,29},30{31accessorKey: 'firstName',32header: 'First Name',33mantineEditTextInputProps: {34type: 'email',35required: true,36error: validationErrors?.firstName,37//remove any previous validation errors when user focuses on the input38onFocus: () =>39setValidationErrors({40...validationErrors,41firstName: undefined,42}),43//optionally add validation checking for onBlur or onChange44},45},46{47accessorKey: 'lastName',48header: 'Last Name',49mantineEditTextInputProps: {50type: 'email',51required: true,52error: validationErrors?.lastName,53//remove any previous validation errors when user focuses on the input54onFocus: () =>55setValidationErrors({56...validationErrors,57lastName: undefined,58}),59},60},61{62accessorKey: 'email',63header: 'Email',64mantineEditTextInputProps: {65type: 'email',66required: true,67error: validationErrors?.email,68//remove any previous validation errors when user focuses on the input69onFocus: () =>70setValidationErrors({71...validationErrors,72email: undefined,73}),74},75},76{77accessorKey: 'state',78header: 'State',79editVariant: 'select',80mantineEditSelectProps: {81data: usStates,82error: validationErrors?.state,83},84},85],86[validationErrors],87);8889//call CREATE hook90const { mutateAsync: createUser, isLoading: isCreatingUser } =91useCreateUser();92//call READ hook93const {94data: fetchedUsers = [],95isError: isLoadingUsersError,96isFetching: isFetchingUsers,97isLoading: isLoadingUsers,98} = useGetUsers();99//call UPDATE hook100const { mutateAsync: updateUser, isLoading: isUpdatingUser } =101useUpdateUser();102//call DELETE hook103const { mutateAsync: deleteUser, isLoading: isDeletingUser } =104useDeleteUser();105106//CREATE action107const handleCreateUser = async ({ values, exitCreatingMode }) => {108const newValidationErrors = validateUser(values);109if (Object.values(newValidationErrors).some((error) => error)) {110setValidationErrors(newValidationErrors);111return;112}113setValidationErrors({});114await createUser(values);115exitCreatingMode();116};117118//UPDATE action119const handleSaveUser = async ({ values, table }) => {120const newValidationErrors = validateUser(values);121if (Object.values(newValidationErrors).some((error) => error)) {122setValidationErrors(newValidationErrors);123return;124}125setValidationErrors({});126await updateUser(values);127table.setEditingRow(null); //exit editing mode128};129130//DELETE action131const openDeleteConfirmModal = (row) =>132modals.openConfirmModal({133title: 'Are you sure you want to delete this user?',134children: (135<Text>136Are you sure you want to delete {row.original.firstName}{' '}137{row.original.lastName}? This action cannot be undone.138</Text>139),140labels: { confirm: 'Delete', cancel: 'Cancel' },141confirmProps: { color: 'red' },142onConfirm: () => deleteUser(row.original.id),143});144145const table = useMantineReactTable({146columns,147data: fetchedUsers,148createDisplayMode: 'row', // ('modal', and 'custom' are also available)149editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)150enableEditing: true,151getRowId: (row) => row.id,152mantineToolbarAlertBannerProps: isLoadingUsersError153? {154color: 'red',155children: 'Error loading data',156}157: undefined,158mantineTableContainerProps: {159sx: {160minHeight: '500px',161},162},163onCreatingRowCancel: () => setValidationErrors({}),164onCreatingRowSave: handleCreateUser,165onEditingRowCancel: () => setValidationErrors({}),166onEditingRowSave: handleSaveUser,167renderRowActions: ({ row, table }) => (168<Flex gap="md">169<Tooltip label="Edit">170<ActionIcon onClick={() => table.setEditingRow(row)}>171<IconEdit />172</ActionIcon>173</Tooltip>174<Tooltip label="Delete">175<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>176<IconTrash />177</ActionIcon>178</Tooltip>179</Flex>180),181renderTopToolbarCustomActions: ({ table }) => (182<Button183onClick={() => {184table.setCreatingRow(true); //simplest way to open the create row modal with no default values185//or you can pass in a row object to set default values with the `createRow` helper function186// table.setCreatingRow(187// createRow(table, {188// //optionally pass in default values for the new row, useful for nested data or other complex scenarios189// }),190// );191}}192>193Create New User194</Button>195),196state: {197isLoading: isLoadingUsers,198isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,199showAlertBanner: isLoadingUsersError,200showProgressBars: isFetchingUsers,201},202});203204return <MantineReactTable table={table} />;205};206207//CREATE hook (post new user to api)208function useCreateUser() {209const queryClient = useQueryClient();210return useMutation({211mutationFn: async (user) => {212//send api update request here213await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call214return Promise.resolve();215},216//client side optimistic update217onMutate: (newUserInfo) => {218queryClient.setQueryData(['users'], (prevUsers) => [219...prevUsers,220{221...newUserInfo,222id: (Math.random() + 1).toString(36).substring(7),223},224]);225},226// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo227});228}229230//READ hook (get users from api)231function useGetUsers() {232return useQuery({233queryKey: ['users'],234queryFn: async () => {235//send api request here236await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call237return Promise.resolve(fakeData);238},239refetchOnWindowFocus: false,240});241}242243//UPDATE hook (put user in api)244function useUpdateUser() {245const queryClient = useQueryClient();246return useMutation({247mutationFn: async (user) => {248//send api update request here249await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call250return Promise.resolve();251},252//client side optimistic update253onMutate: (newUserInfo) => {254queryClient.setQueryData(['users'], (prevUsers) =>255prevUsers?.map((prevUser) =>256prevUser.id === newUserInfo.id ? newUserInfo : prevUser,257),258);259},260// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo261});262}263264//DELETE hook (delete user in api)265function useDeleteUser() {266const queryClient = useQueryClient();267return useMutation({268mutationFn: async (userId) => {269//send api update request here270await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call271return Promise.resolve();272},273//client side optimistic update274onMutate: (userId) => {275queryClient.setQueryData(['users'], (prevUsers) =>276prevUsers?.filter((user) => user.id !== userId),277);278},279// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo280});281}282283const queryClient = new QueryClient();284285const ExampleWithProviders = () => (286//Put this with your other react-query providers near root of your app287<QueryClientProvider client={queryClient}>288<ModalsProvider>289<Example />290</ModalsProvider>291</QueryClientProvider>292);293294export default ExampleWithProviders;295296const validateRequired = (value) => !!value.length;297const validateEmail = (email) =>298!!email.length &&299300.toLowerCase()301.match(302/^(([^<>()[\]\\.,;:\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,}))$/,303);304305function validateUser(user) {306return {307firstName: !validateRequired(user.firstName)308? 'First Name is Required'309: '',310lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',311email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',312};313}
View Extra Storybook Examples