Editing (CRUD) 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 default
"modal"
editing mode, where a dialog opens up to edit 1 row at a time.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 {3MRT_EditActionButtons,4MantineReactTable,5// createRow,6type MRT_ColumnDef,7type MRT_Row,8type MRT_TableOptions,9useMantineReactTable,10} from 'mantine-react-table';11import {12ActionIcon,13Button,14Flex,15Stack,16Text,17Title,18Tooltip,19} from '@mantine/core';20import { ModalsProvider, modals } from '@mantine/modals';21import { IconEdit, IconTrash } from '@tabler/icons-react';22import {23QueryClient,24QueryClientProvider,25useMutation,26useQuery,27useQueryClient,28} from '@tanstack/react-query';29import { type User, fakeData, usStates } from './makeData';3031const Example = () => {32const [validationErrors, setValidationErrors] = useState<33Record<string, string | undefined>34>({});3536const columns = useMemo<MRT_ColumnDef<User>[]>(37() => [38{39accessorKey: 'id',40header: 'Id',41enableEditing: false,42size: 80,43},44{45accessorKey: 'firstName',46header: 'First Name',47mantineEditTextInputProps: {48type: 'email',49required: true,50error: validationErrors?.firstName,51//remove any previous validation errors when user focuses on the input52onFocus: () =>53setValidationErrors({54...validationErrors,55firstName: undefined,56}),57//optionally add validation checking for onBlur or onChange58},59},60{61accessorKey: 'lastName',62header: 'Last Name',63mantineEditTextInputProps: {64type: 'email',65required: true,66error: validationErrors?.lastName,67//remove any previous validation errors when user focuses on the input68onFocus: () =>69setValidationErrors({70...validationErrors,71lastName: undefined,72}),73},74},75{76accessorKey: 'email',77header: 'Email',78mantineEditTextInputProps: {79type: 'email',80required: true,81error: validationErrors?.email,82//remove any previous validation errors when user focuses on the input83onFocus: () =>84setValidationErrors({85...validationErrors,86email: undefined,87}),88},89},90{91accessorKey: 'state',92header: 'State',93editVariant: 'select',94mantineEditSelectProps: {95data: usStates,96error: validationErrors?.state,97},98},99],100[validationErrors],101);102103//call CREATE hook104const { mutateAsync: createUser, isLoading: isCreatingUser } =105useCreateUser();106//call READ hook107const {108data: fetchedUsers = [],109isError: isLoadingUsersError,110isFetching: isFetchingUsers,111isLoading: isLoadingUsers,112} = useGetUsers();113//call UPDATE hook114const { mutateAsync: updateUser, isLoading: isUpdatingUser } =115useUpdateUser();116//call DELETE hook117const { mutateAsync: deleteUser, isLoading: isDeletingUser } =118useDeleteUser();119120//CREATE action121const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({122values,123exitCreatingMode,124}) => {125const newValidationErrors = validateUser(values);126if (Object.values(newValidationErrors).some((error) => error)) {127setValidationErrors(newValidationErrors);128return;129}130setValidationErrors({});131await createUser(values);132exitCreatingMode();133};134135//UPDATE action136const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({137values,138table,139}) => {140const newValidationErrors = validateUser(values);141if (Object.values(newValidationErrors).some((error) => error)) {142setValidationErrors(newValidationErrors);143return;144}145setValidationErrors({});146await updateUser(values);147table.setEditingRow(null); //exit editing mode148};149150//DELETE action151const openDeleteConfirmModal = (row: MRT_Row<User>) =>152modals.openConfirmModal({153title: 'Are you sure you want to delete this user?',154children: (155<Text>156Are you sure you want to delete {row.original.firstName}{' '}157{row.original.lastName}? This action cannot be undone.158</Text>159),160labels: { confirm: 'Delete', cancel: 'Cancel' },161confirmProps: { color: 'red' },162onConfirm: () => deleteUser(row.original.id),163});164165const table = useMantineReactTable({166columns,167data: fetchedUsers,168createDisplayMode: 'modal', //default ('row', and 'custom' are also available)169editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)170enableEditing: true,171getRowId: (row) => row.id,172mantineToolbarAlertBannerProps: isLoadingUsersError173? {174color: 'red',175children: 'Error loading data',176}177: undefined,178mantineTableContainerProps: {179sx: {180minHeight: '500px',181},182},183onCreatingRowCancel: () => setValidationErrors({}),184onCreatingRowSave: handleCreateUser,185onEditingRowCancel: () => setValidationErrors({}),186onEditingRowSave: handleSaveUser,187renderCreateRowModalContent: ({ table, row, internalEditComponents }) => (188<Stack>189<Title order={3}>Create New User</Title>190{internalEditComponents}191<Flex justify="flex-end" mt="xl">192<MRT_EditActionButtons variant="text" table={table} row={row} />193</Flex>194</Stack>195),196renderEditRowModalContent: ({ table, row, internalEditComponents }) => (197<Stack>198<Title order={3}>Edit User</Title>199{internalEditComponents}200<Flex justify="flex-end" mt="xl">201<MRT_EditActionButtons variant="text" table={table} row={row} />202</Flex>203</Stack>204),205renderRowActions: ({ row, table }) => (206<Flex gap="md">207<Tooltip label="Edit">208<ActionIcon onClick={() => table.setEditingRow(row)}>209<IconEdit />210</ActionIcon>211</Tooltip>212<Tooltip label="Delete">213<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>214<IconTrash />215</ActionIcon>216</Tooltip>217</Flex>218),219renderTopToolbarCustomActions: ({ table }) => (220<Button221onClick={() => {222table.setCreatingRow(true); //simplest way to open the create row modal with no default values223//or you can pass in a row object to set default values with the `createRow` helper function224// table.setCreatingRow(225// createRow(table, {226// //optionally pass in default values for the new row, useful for nested data or other complex scenarios227// }),228// );229}}230>231Create New User232</Button>233),234state: {235isLoading: isLoadingUsers,236isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,237showAlertBanner: isLoadingUsersError,238showProgressBars: isFetchingUsers,239},240});241242return <MantineReactTable table={table} />;243};244245//CREATE hook (post new user to api)246function useCreateUser() {247const queryClient = useQueryClient();248return useMutation({249mutationFn: async (user: User) => {250//send api update request here251await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call252return Promise.resolve();253},254//client side optimistic update255onMutate: (newUserInfo: User) => {256queryClient.setQueryData(257['users'],258(prevUsers: any) =>259[260...prevUsers,261{262...newUserInfo,263id: (Math.random() + 1).toString(36).substring(7),264},265] as User[],266);267},268// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo269});270}271272//READ hook (get users from api)273function useGetUsers() {274return useQuery<User[]>({275queryKey: ['users'],276queryFn: async () => {277//send api request here278await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call279return Promise.resolve(fakeData);280},281refetchOnWindowFocus: false,282});283}284285//UPDATE hook (put user in api)286function useUpdateUser() {287const queryClient = useQueryClient();288return useMutation({289mutationFn: async (user: User) => {290//send api update request here291await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call292return Promise.resolve();293},294//client side optimistic update295onMutate: (newUserInfo: User) => {296queryClient.setQueryData(297['users'],298(prevUsers: any) =>299prevUsers?.map((prevUser: User) =>300prevUser.id === newUserInfo.id ? newUserInfo : prevUser,301),302);303},304// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo305});306}307308//DELETE hook (delete user in api)309function useDeleteUser() {310const queryClient = useQueryClient();311return useMutation({312mutationFn: async (userId: string) => {313//send api update request here314await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call315return Promise.resolve();316},317//client side optimistic update318onMutate: (userId: string) => {319queryClient.setQueryData(320['users'],321(prevUsers: any) =>322prevUsers?.filter((user: User) => user.id !== userId),323);324},325// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo326});327}328329const queryClient = new QueryClient();330331const ExampleWithProviders = () => (332//Put this with your other react-query providers near root of your app333<QueryClientProvider client={queryClient}>334<ModalsProvider>335<Example />336</ModalsProvider>337</QueryClientProvider>338);339340export default ExampleWithProviders;341342const validateRequired = (value: string) => !!value.length;343const validateEmail = (email: string) =>344!!email.length &&345346.toLowerCase()347.match(348/^(([^<>()[\]\\.,;:\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,}))$/,349);350351function validateUser(user: User) {352return {353firstName: !validateRequired(user.firstName)354? 'First Name is Required'355: '',356lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',357email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',358};359}
1import { useMemo, useState } from 'react';2import {3MRT_EditActionButtons,4MantineReactTable,5// createRow,6useMantineReactTable,7} from 'mantine-react-table';8import {9ActionIcon,10Button,11Flex,12Stack,13Text,14Title,15Tooltip,16} from '@mantine/core';17import { ModalsProvider, modals } from '@mantine/modals';18import { IconEdit, IconTrash } from '@tabler/icons-react';19import {20QueryClient,21QueryClientProvider,22useMutation,23useQuery,24useQueryClient,25} from '@tanstack/react-query';26import { fakeData, usStates } from './makeData';2728const Example = () => {29const [validationErrors, setValidationErrors] = useState({});3031const columns = useMemo(32() => [33{34accessorKey: 'id',35header: 'Id',36enableEditing: false,37size: 80,38},39{40accessorKey: 'firstName',41header: 'First Name',42mantineEditTextInputProps: {43type: 'email',44required: true,45error: validationErrors?.firstName,46//remove any previous validation errors when user focuses on the input47onFocus: () =>48setValidationErrors({49...validationErrors,50firstName: undefined,51}),52//optionally add validation checking for onBlur or onChange53},54},55{56accessorKey: 'lastName',57header: 'Last Name',58mantineEditTextInputProps: {59type: 'email',60required: true,61error: validationErrors?.lastName,62//remove any previous validation errors when user focuses on the input63onFocus: () =>64setValidationErrors({65...validationErrors,66lastName: undefined,67}),68},69},70{71accessorKey: 'email',72header: 'Email',73mantineEditTextInputProps: {74type: 'email',75required: true,76error: validationErrors?.email,77//remove any previous validation errors when user focuses on the input78onFocus: () =>79setValidationErrors({80...validationErrors,81email: undefined,82}),83},84},85{86accessorKey: 'state',87header: 'State',88editVariant: 'select',89mantineEditSelectProps: {90data: usStates,91error: validationErrors?.state,92},93},94],95[validationErrors],96);9798//call CREATE hook99const { mutateAsync: createUser, isLoading: isCreatingUser } =100useCreateUser();101//call READ hook102const {103data: fetchedUsers = [],104isError: isLoadingUsersError,105isFetching: isFetchingUsers,106isLoading: isLoadingUsers,107} = useGetUsers();108//call UPDATE hook109const { mutateAsync: updateUser, isLoading: isUpdatingUser } =110useUpdateUser();111//call DELETE hook112const { mutateAsync: deleteUser, isLoading: isDeletingUser } =113useDeleteUser();114115//CREATE action116const handleCreateUser = async ({ values, exitCreatingMode }) => {117const newValidationErrors = validateUser(values);118if (Object.values(newValidationErrors).some((error) => error)) {119setValidationErrors(newValidationErrors);120return;121}122setValidationErrors({});123await createUser(values);124exitCreatingMode();125};126127//UPDATE action128const handleSaveUser = async ({ values, table }) => {129const newValidationErrors = validateUser(values);130if (Object.values(newValidationErrors).some((error) => error)) {131setValidationErrors(newValidationErrors);132return;133}134setValidationErrors({});135await updateUser(values);136table.setEditingRow(null); //exit editing mode137};138139//DELETE action140const openDeleteConfirmModal = (row) =>141modals.openConfirmModal({142title: 'Are you sure you want to delete this user?',143children: (144<Text>145Are you sure you want to delete {row.original.firstName}{' '}146{row.original.lastName}? This action cannot be undone.147</Text>148),149labels: { confirm: 'Delete', cancel: 'Cancel' },150confirmProps: { color: 'red' },151onConfirm: () => deleteUser(row.original.id),152});153154const table = useMantineReactTable({155columns,156data: fetchedUsers,157createDisplayMode: 'modal', //default ('row', and 'custom' are also available)158editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)159enableEditing: true,160getRowId: (row) => row.id,161mantineToolbarAlertBannerProps: isLoadingUsersError162? {163color: 'red',164children: 'Error loading data',165}166: undefined,167mantineTableContainerProps: {168sx: {169minHeight: '500px',170},171},172onCreatingRowCancel: () => setValidationErrors({}),173onCreatingRowSave: handleCreateUser,174onEditingRowCancel: () => setValidationErrors({}),175onEditingRowSave: handleSaveUser,176renderCreateRowModalContent: ({ table, row, internalEditComponents }) => (177<Stack>178<Title order={3}>Create New User</Title>179{internalEditComponents}180<Flex justify="flex-end" mt="xl">181<MRT_EditActionButtons variant="text" table={table} row={row} />182</Flex>183</Stack>184),185renderEditRowModalContent: ({ table, row, internalEditComponents }) => (186<Stack>187<Title order={3}>Edit User</Title>188{internalEditComponents}189<Flex justify="flex-end" mt="xl">190<MRT_EditActionButtons variant="text" table={table} row={row} />191</Flex>192</Stack>193),194renderRowActions: ({ row, table }) => (195<Flex gap="md">196<Tooltip label="Edit">197<ActionIcon onClick={() => table.setEditingRow(row)}>198<IconEdit />199</ActionIcon>200</Tooltip>201<Tooltip label="Delete">202<ActionIcon color="red" onClick={() => openDeleteConfirmModal(row)}>203<IconTrash />204</ActionIcon>205</Tooltip>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 user in api)271function useUpdateUser() {272const queryClient = useQueryClient();273return useMutation({274mutationFn: async (user) => {275//send api update request here276await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call277return Promise.resolve();278},279//client side optimistic update280onMutate: (newUserInfo) => {281queryClient.setQueryData(['users'], (prevUsers) =>282prevUsers?.map((prevUser) =>283prevUser.id === newUserInfo.id ? newUserInfo : prevUser,284),285);286},287// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo288});289}290291//DELETE hook (delete user in api)292function useDeleteUser() {293const queryClient = useQueryClient();294return useMutation({295mutationFn: async (userId) => {296//send api update request here297await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call298return Promise.resolve();299},300//client side optimistic update301onMutate: (userId) => {302queryClient.setQueryData(['users'], (prevUsers) =>303prevUsers?.filter((user) => user.id !== userId),304);305},306// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo307});308}309310const queryClient = new QueryClient();311312const ExampleWithProviders = () => (313//Put this with your other react-query providers near root of your app314<QueryClientProvider client={queryClient}>315<ModalsProvider>316<Example />317</ModalsProvider>318</QueryClientProvider>319);320321export default ExampleWithProviders;322323const validateRequired = (value) => !!value.length;324const validateEmail = (email) =>325!!email.length &&326327.toLowerCase()328.match(329/^(([^<>()[\]\\.,;:\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,}))$/,330);331332function validateUser(user) {333return {334firstName: !validateRequired(user.firstName)335? 'First Name is Required'336: '',337lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',338email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',339};340}
View Extra Storybook Examples