Editing (CRUD) Inline Row Example
Full CRUD (Create, Read, Update, Delete) functionality can be easily implemented with Material 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 |
---|---|---|---|---|---|
10
1import { useMemo, useState } from 'react';2import {3 MaterialReactTable,4 // createRow,5 type MRT_ColumnDef,6 type MRT_Row,7 type MRT_TableOptions,8 useMaterialReactTable,9} from 'material-react-table';10import { Box, Button, IconButton, Tooltip } from '@mui/material';11import {12 QueryClient,13 QueryClientProvider,14 useMutation,15 useQuery,16 useQueryClient,17} from '@tanstack/react-query';18import { type User, fakeData, usStates } from './makeData';19import EditIcon from '@mui/icons-material/Edit';20import DeleteIcon from '@mui/icons-material/Delete';2122const Example = () => {23 const [validationErrors, setValidationErrors] = useState<24 Record<string, string | undefined>25 >({});2627 const columns = useMemo<MRT_ColumnDef<User>[]>(28 () => [29 {30 accessorKey: 'id',31 header: 'Id',32 enableEditing: false,33 size: 80,34 },35 {36 accessorKey: 'firstName',37 header: 'First Name',38 muiEditTextFieldProps: {39 required: true,40 error: !!validationErrors?.firstName,41 helperText: validationErrors?.firstName,42 //remove any previous validation errors when user focuses on the input43 onFocus: () =>44 setValidationErrors({45 ...validationErrors,46 firstName: undefined,47 }),48 //optionally add validation checking for onBlur or onChange49 },50 },51 {52 accessorKey: 'lastName',53 header: 'Last Name',54 muiEditTextFieldProps: {55 required: true,56 error: !!validationErrors?.lastName,57 helperText: validationErrors?.lastName,58 //remove any previous validation errors when user focuses on the input59 onFocus: () =>60 setValidationErrors({61 ...validationErrors,62 lastName: undefined,63 }),64 },65 },66 {67 accessorKey: 'email',68 header: 'Email',69 muiEditTextFieldProps: {70 type: 'email',71 required: true,72 error: !!validationErrors?.email,73 helperText: validationErrors?.email,74 //remove any previous validation errors when user focuses on the input75 onFocus: () =>76 setValidationErrors({77 ...validationErrors,78 email: undefined,79 }),80 },81 },82 {83 accessorKey: 'state',84 header: 'State',85 editVariant: 'select',86 editSelectOptions: usStates,87 muiEditTextFieldProps: {88 select: true,89 error: !!validationErrors?.state,90 helperText: validationErrors?.state,91 },92 },93 ],94 [validationErrors],95 );9697 //call CREATE hook98 const { mutateAsync: createUser, isPending: isCreatingUser } =99 useCreateUser();100 //call READ hook101 const {102 data: fetchedUsers = [],103 isError: isLoadingUsersError,104 isFetching: isFetchingUsers,105 isLoading: isLoadingUsers,106 } = useGetUsers();107 //call UPDATE hook108 const { mutateAsync: updateUser, isPending: isUpdatingUser } =109 useUpdateUser();110 //call DELETE hook111 const { mutateAsync: deleteUser, isPending: isDeletingUser } =112 useDeleteUser();113114 //CREATE action115 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({116 values,117 table,118 }) => {119 const newValidationErrors = validateUser(values);120 if (Object.values(newValidationErrors).some((error) => error)) {121 setValidationErrors(newValidationErrors);122 return;123 }124 setValidationErrors({});125 await createUser(values);126 table.setCreatingRow(null); //exit creating mode127 };128129 //UPDATE action130 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({131 values,132 table,133 }) => {134 const newValidationErrors = validateUser(values);135 if (Object.values(newValidationErrors).some((error) => error)) {136 setValidationErrors(newValidationErrors);137 return;138 }139 setValidationErrors({});140 await updateUser(values);141 table.setEditingRow(null); //exit editing mode142 };143144 //DELETE action145 const openDeleteConfirmModal = (row: MRT_Row<User>) => {146 if (window.confirm('Are you sure you want to delete this user?')) {147 deleteUser(row.original.id);148 }149 };150151 const table = useMaterialReactTable({152 columns,153 data: fetchedUsers,154 createDisplayMode: 'row', // ('modal', and 'custom' are also available)155 editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)156 enableEditing: true,157 getRowId: (row) => row.id,158 muiToolbarAlertBannerProps: isLoadingUsersError159 ? {160 color: 'error',161 children: 'Error loading data',162 }163 : undefined,164 muiTableContainerProps: {165 sx: {166 minHeight: '500px',167 },168 },169 onCreatingRowCancel: () => setValidationErrors({}),170 onCreatingRowSave: handleCreateUser,171 onEditingRowCancel: () => setValidationErrors({}),172 onEditingRowSave: handleSaveUser,173 renderRowActions: ({ row, table }) => (174 <Box sx={{ display: 'flex', gap: '1rem' }}>175 <Tooltip title="Edit">176 <IconButton onClick={() => table.setEditingRow(row)}>177 <EditIcon />178 </IconButton>179 </Tooltip>180 <Tooltip title="Delete">181 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>182 <DeleteIcon />183 </IconButton>184 </Tooltip>185 </Box>186 ),187 renderTopToolbarCustomActions: ({ table }) => (188 <Button189 variant="contained"190 onClick={() => {191 table.setCreatingRow(true); //simplest way to open the create row modal with no default values192 //or you can pass in a row object to set default values with the `createRow` helper function193 // table.setCreatingRow(194 // createRow(table, {195 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios196 // }),197 // );198 }}199 >200 Create New User201 </Button>202 ),203 state: {204 isLoading: isLoadingUsers,205 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,206 showAlertBanner: isLoadingUsersError,207 showProgressBars: isFetchingUsers,208 },209 });210211 return <MaterialReactTable table={table} />;212};213214//CREATE hook (post new user to api)215function useCreateUser() {216 const queryClient = useQueryClient();217 return useMutation({218 mutationFn: async (user: User) => {219 //send api update request here220 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call221 return Promise.resolve();222 },223 //client side optimistic update224 onMutate: (newUserInfo: User) => {225 queryClient.setQueryData(226 ['users'],227 (prevUsers: any) =>228 [229 ...prevUsers,230 {231 ...newUserInfo,232 id: (Math.random() + 1).toString(36).substring(7),233 },234 ] as User[],235 );236 },237 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo238 });239}240241//READ hook (get users from api)242function useGetUsers() {243 return useQuery<User[]>({244 queryKey: ['users'],245 queryFn: async () => {246 //send api request here247 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call248 return Promise.resolve(fakeData);249 },250 refetchOnWindowFocus: false,251 });252}253254//UPDATE hook (put user in api)255function useUpdateUser() {256 const queryClient = useQueryClient();257 return useMutation({258 mutationFn: async (user: User) => {259 //send api update request here260 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call261 return Promise.resolve();262 },263 //client side optimistic update264 onMutate: (newUserInfo: User) => {265 queryClient.setQueryData(['users'], (prevUsers: any) =>266 prevUsers?.map((prevUser: User) =>267 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,268 ),269 );270 },271 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo272 });273}274275//DELETE hook (delete user in api)276function useDeleteUser() {277 const queryClient = useQueryClient();278 return useMutation({279 mutationFn: async (userId: string) => {280 //send api update request here281 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call282 return Promise.resolve();283 },284 //client side optimistic update285 onMutate: (userId: string) => {286 queryClient.setQueryData(['users'], (prevUsers: any) =>287 prevUsers?.filter((user: User) => user.id !== userId),288 );289 },290 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo291 });292}293294const queryClient = new QueryClient();295296const ExampleWithProviders = () => (297 //Put this with your other react-query providers near root of your app298 <QueryClientProvider client={queryClient}>299 <Example />300 </QueryClientProvider>301);302303export default ExampleWithProviders;304305const validateRequired = (value: string) => !!value.length;306const validateEmail = (email: string) =>307 !!email.length &&308 email309 .toLowerCase()310 .match(311 /^(([^<>()[\]\\.,;:\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,}))$/,312 );313314function validateUser(user: User) {315 return {316 firstName: !validateRequired(user.firstName)317 ? 'First Name is Required'318 : '',319 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',320 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',321 };322}323
View Extra Storybook Examples