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
1-10 of 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 type: 'email',40 required: true,41 error: !!validationErrors?.firstName,42 helperText: validationErrors?.firstName,43 //remove any previous validation errors when user focuses on the input44 onFocus: () =>45 setValidationErrors({46 ...validationErrors,47 firstName: undefined,48 }),49 //optionally add validation checking for onBlur or onChange50 },51 },52 {53 accessorKey: 'lastName',54 header: 'Last Name',55 muiEditTextFieldProps: {56 type: 'email',57 required: true,58 error: !!validationErrors?.lastName,59 helperText: validationErrors?.lastName,60 //remove any previous validation errors when user focuses on the input61 onFocus: () =>62 setValidationErrors({63 ...validationErrors,64 lastName: undefined,65 }),66 },67 },68 {69 accessorKey: 'email',70 header: 'Email',71 muiEditTextFieldProps: {72 type: 'email',73 required: true,74 error: !!validationErrors?.email,75 helperText: validationErrors?.email,76 //remove any previous validation errors when user focuses on the input77 onFocus: () =>78 setValidationErrors({79 ...validationErrors,80 email: undefined,81 }),82 },83 },84 {85 accessorKey: 'state',86 header: 'State',87 editVariant: 'select',88 editSelectOptions: usStates,89 muiEditTextFieldProps: {90 select: true,91 error: !!validationErrors?.state,92 helperText: validationErrors?.state,93 },94 },95 ],96 [validationErrors],97 );9899 //call CREATE hook100 const { mutateAsync: createUser, isPending: isCreatingUser } =101 useCreateUser();102 //call READ hook103 const {104 data: fetchedUsers = [],105 isError: isLoadingUsersError,106 isFetching: isFetchingUsers,107 isLoading: isLoadingUsers,108 } = useGetUsers();109 //call UPDATE hook110 const { mutateAsync: updateUser, isPending: isUpdatingUser } =111 useUpdateUser();112 //call DELETE hook113 const { mutateAsync: deleteUser, isPending: isDeletingUser } =114 useDeleteUser();115116 //CREATE action117 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({118 values,119 table,120 }) => {121 const newValidationErrors = validateUser(values);122 if (Object.values(newValidationErrors).some((error) => error)) {123 setValidationErrors(newValidationErrors);124 return;125 }126 setValidationErrors({});127 await createUser(values);128 table.setCreatingRow(null); //exit creating mode129 };130131 //UPDATE action132 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({133 values,134 table,135 }) => {136 const newValidationErrors = validateUser(values);137 if (Object.values(newValidationErrors).some((error) => error)) {138 setValidationErrors(newValidationErrors);139 return;140 }141 setValidationErrors({});142 await updateUser(values);143 table.setEditingRow(null); //exit editing mode144 };145146 //DELETE action147 const openDeleteConfirmModal = (row: MRT_Row<User>) => {148 if (window.confirm('Are you sure you want to delete this user?')) {149 deleteUser(row.original.id);150 }151 };152153 const table = useMaterialReactTable({154 columns,155 data: fetchedUsers,156 createDisplayMode: 'row', // ('modal', and 'custom' are also available)157 editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)158 enableEditing: true,159 getRowId: (row) => row.id,160 muiToolbarAlertBannerProps: isLoadingUsersError161 ? {162 color: 'error',163 children: 'Error loading data',164 }165 : undefined,166 muiTableContainerProps: {167 sx: {168 minHeight: '500px',169 },170 },171 onCreatingRowCancel: () => setValidationErrors({}),172 onCreatingRowSave: handleCreateUser,173 onEditingRowCancel: () => setValidationErrors({}),174 onEditingRowSave: handleSaveUser,175 renderRowActions: ({ row, table }) => (176 <Box sx={{ display: 'flex', gap: '1rem' }}>177 <Tooltip title="Edit">178 <IconButton onClick={() => table.setEditingRow(row)}>179 <EditIcon />180 </IconButton>181 </Tooltip>182 <Tooltip title="Delete">183 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>184 <DeleteIcon />185 </IconButton>186 </Tooltip>187 </Box>188 ),189 renderTopToolbarCustomActions: ({ table }) => (190 <Button191 variant="contained"192 onClick={() => {193 table.setCreatingRow(true); //simplest way to open the create row modal with no default values194 //or you can pass in a row object to set default values with the `createRow` helper function195 // table.setCreatingRow(196 // createRow(table, {197 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios198 // }),199 // );200 }}201 >202 Create New User203 </Button>204 ),205 state: {206 isLoading: isLoadingUsers,207 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,208 showAlertBanner: isLoadingUsersError,209 showProgressBars: isFetchingUsers,210 },211 });212213 return <MaterialReactTable table={table} />;214};215216//CREATE hook (post new user to api)217function useCreateUser() {218 const queryClient = useQueryClient();219 return useMutation({220 mutationFn: async (user: User) => {221 //send api update request here222 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call223 return Promise.resolve();224 },225 //client side optimistic update226 onMutate: (newUserInfo: User) => {227 queryClient.setQueryData(228 ['users'],229 (prevUsers: any) =>230 [231 ...prevUsers,232 {233 ...newUserInfo,234 id: (Math.random() + 1).toString(36).substring(7),235 },236 ] as User[],237 );238 },239 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo240 });241}242243//READ hook (get users from api)244function useGetUsers() {245 return useQuery<User[]>({246 queryKey: ['users'],247 queryFn: async () => {248 //send api request here249 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call250 return Promise.resolve(fakeData);251 },252 refetchOnWindowFocus: false,253 });254}255256//UPDATE hook (put user in api)257function useUpdateUser() {258 const queryClient = useQueryClient();259 return useMutation({260 mutationFn: async (user: User) => {261 //send api update request here262 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call263 return Promise.resolve();264 },265 //client side optimistic update266 onMutate: (newUserInfo: User) => {267 queryClient.setQueryData(268 ['users'],269 (prevUsers: any) =>270 prevUsers?.map((prevUser: User) =>271 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,272 ),273 );274 },275 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo276 });277}278279//DELETE hook (delete user in api)280function useDeleteUser() {281 const queryClient = useQueryClient();282 return useMutation({283 mutationFn: async (userId: string) => {284 //send api update request here285 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call286 return Promise.resolve();287 },288 //client side optimistic update289 onMutate: (userId: string) => {290 queryClient.setQueryData(291 ['users'],292 (prevUsers: any) =>293 prevUsers?.filter((user: User) => user.id !== userId),294 );295 },296 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo297 });298}299300const queryClient = new QueryClient();301302const ExampleWithProviders = () => (303 //Put this with your other react-query providers near root of your app304 <QueryClientProvider client={queryClient}>305 <Example />306 </QueryClientProvider>307);308309export default ExampleWithProviders;310311const validateRequired = (value: string) => !!value.length;312const validateEmail = (email: string) =>313 !!email.length &&314 email315 .toLowerCase()316 .match(317 /^(([^<>()[\]\\.,;:\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,}))$/,318 );319320function validateUser(user: User) {321 return {322 firstName: !validateRequired(user.firstName)323 ? 'First Name is Required'324 : '',325 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',326 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',327 };328}329
View Extra Storybook Examples