Editing (CRUD) Inline Cell 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 "cell"
editing mode, which allows you to edit a single cell at a time. 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 |
---|---|---|---|---|---|
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 {11 Box,12 Button,13 CircularProgress,14 IconButton,15 Tooltip,16 Typography,17} from '@mui/material';18import {19 QueryClient,20 QueryClientProvider,21 useMutation,22 useQuery,23 useQueryClient,24} from '@tanstack/react-query';25import { type User, fakeData, usStates } from './makeData';26import EditIcon from '@mui/icons-material/Edit';27import DeleteIcon from '@mui/icons-material/Delete';2829const Example = () => {30 const [validationErrors, setValidationErrors] = useState<31 Record<string, string | undefined>32 >({});33 //keep track of rows that have been edited34 const [editedUsers, setEditedUsers] = useState<Record<string, User>>({});3536 const columns = useMemo<MRT_ColumnDef<User>[]>(37 () => [38 {39 accessorKey: 'id',40 header: 'Id',41 enableEditing: false,42 size: 80,43 },44 {45 accessorKey: 'firstName',46 header: 'First Name',47 muiEditTextFieldProps: ({ cell, row }) => ({48 type: 'text',49 required: true,50 error: !!validationErrors?.[cell.id],51 helperText: validationErrors?.[cell.id],52 //store edited user in state to be saved later53 onBlur: (event) => {54 const validationError = !validateRequired(event.currentTarget.value)55 ? 'Required'56 : undefined;57 setValidationErrors({58 ...validationErrors,59 [cell.id]: validationError,60 });61 setEditedUsers({ ...editedUsers, [row.id]: row.original });62 },63 }),64 },65 {66 accessorKey: 'lastName',67 header: 'Last Name',68 muiEditTextFieldProps: ({ cell, row }) => ({69 type: 'text',70 required: true,71 error: !!validationErrors?.[cell.id],72 helperText: validationErrors?.[cell.id],73 //store edited user in state to be saved later74 onBlur: (event) => {75 const validationError = !validateRequired(event.currentTarget.value)76 ? 'Required'77 : undefined;78 setValidationErrors({79 ...validationErrors,80 [cell.id]: validationError,81 });82 setEditedUsers({ ...editedUsers, [row.id]: row.original });83 },84 }),85 },86 {87 accessorKey: 'email',88 header: 'Email',89 muiEditTextFieldProps: ({ cell, row }) => ({90 type: 'email',91 required: true,92 error: !!validationErrors?.[cell.id],93 helperText: validationErrors?.[cell.id],94 //store edited user in state to be saved later95 onBlur: (event) => {96 const validationError = !validateEmail(event.currentTarget.value)97 ? 'Incorrect Email Format'98 : undefined;99 setValidationErrors({100 ...validationErrors,101 [cell.id]: validationError,102 });103 setEditedUsers({ ...editedUsers, [row.id]: row.original });104 },105 }),106 },107 {108 accessorKey: 'state',109 header: 'State',110 editVariant: 'select',111 editSelectOptions: usStates,112 muiEditTextFieldProps: ({ row }) => ({113 select: true,114 error: !!validationErrors?.state,115 helperText: validationErrors?.state,116 onChange: (event) =>117 setEditedUsers({118 ...editedUsers,119 [row.id]: { ...row.original, state: event.target.value },120 }),121 }),122 },123 ],124 [editedUsers, validationErrors],125 );126127 //call CREATE hook128 const { mutateAsync: createUser, isPending: isCreatingUser } =129 useCreateUser();130 //call READ hook131 const {132 data: fetchedUsers = [],133 isError: isLoadingUsersError,134 isFetching: isFetchingUsers,135 isLoading: isLoadingUsers,136 } = useGetUsers();137 //call UPDATE hook138 const { mutateAsync: updateUsers, isPending: isUpdatingUsers } =139 useUpdateUsers();140 //call DELETE hook141 const { mutateAsync: deleteUser, isPending: isDeletingUser } =142 useDeleteUser();143144 //CREATE action145 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({146 values,147 table,148 }) => {149 const newValidationErrors = validateUser(values);150 if (Object.values(newValidationErrors).some((error) => error)) {151 setValidationErrors(newValidationErrors);152 return;153 }154 setValidationErrors({});155 await createUser(values);156 table.setCreatingRow(null); //exit creating mode157 };158159 //UPDATE action160 const handleSaveUsers = async () => {161 if (Object.values(validationErrors).some((error) => !!error)) return;162 await updateUsers(Object.values(editedUsers));163 setEditedUsers({});164 };165166 //DELETE action167 const openDeleteConfirmModal = (row: MRT_Row<User>) => {168 if (window.confirm('Are you sure you want to delete this user?')) {169 deleteUser(row.original.id);170 }171 };172173 const table = useMaterialReactTable({174 columns,175 data: fetchedUsers,176 createDisplayMode: 'row', // ('modal', and 'custom' are also available)177 editDisplayMode: 'cell', // ('modal', 'row', 'table', and 'custom' are also available)178 enableEditing: true,179 enableRowActions: true,180 positionActionsColumn: 'last',181 getRowId: (row) => row.id,182 muiToolbarAlertBannerProps: isLoadingUsersError183 ? {184 color: 'error',185 children: 'Error loading data',186 }187 : undefined,188 muiTableContainerProps: {189 sx: {190 minHeight: '500px',191 },192 },193 onCreatingRowCancel: () => setValidationErrors({}),194 onCreatingRowSave: handleCreateUser,195 renderRowActions: ({ row }) => (196 <Box sx={{ display: 'flex', gap: '1rem' }}>197 <Tooltip title="Delete">198 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>199 <DeleteIcon />200 </IconButton>201 </Tooltip>202 </Box>203 ),204 renderBottomToolbarCustomActions: () => (205 <Box sx={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>206 <Button207 color="success"208 variant="contained"209 onClick={handleSaveUsers}210 disabled={211 Object.keys(editedUsers).length === 0 ||212 Object.values(validationErrors).some((error) => !!error)213 }214 >215 {isUpdatingUsers ? <CircularProgress size={25} /> : 'Save'}216 </Button>217 {Object.values(validationErrors).some((error) => !!error) && (218 <Typography color="error">Fix errors before submitting</Typography>219 )}220 </Box>221 ),222 renderTopToolbarCustomActions: ({ table }) => (223 <Button224 variant="contained"225 onClick={() => {226 table.setCreatingRow(true); //simplest way to open the create row modal with no default values227 //or you can pass in a row object to set default values with the `createRow` helper function228 // table.setCreatingRow(229 // createRow(table, {230 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios231 // }),232 // );233 }}234 >235 Create New User236 </Button>237 ),238 state: {239 isLoading: isLoadingUsers,240 isSaving: isCreatingUser || isUpdatingUsers || isDeletingUser,241 showAlertBanner: isLoadingUsersError,242 showProgressBars: isFetchingUsers,243 },244 });245246 return <MaterialReactTable table={table} />;247};248249//CREATE hook (post new user to api)250function useCreateUser() {251 const queryClient = useQueryClient();252 return useMutation({253 mutationFn: async (user: User) => {254 //send api update request here255 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call256 return Promise.resolve();257 },258 //client side optimistic update259 onMutate: (newUserInfo: User) => {260 queryClient.setQueryData(261 ['users'],262 (prevUsers: any) =>263 [264 ...prevUsers,265 {266 ...newUserInfo,267 id: (Math.random() + 1).toString(36).substring(7),268 },269 ] as User[],270 );271 },272 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo273 });274}275276//READ hook (get users from api)277function useGetUsers() {278 return useQuery<User[]>({279 queryKey: ['users'],280 queryFn: async () => {281 //send api request here282 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call283 return Promise.resolve(fakeData);284 },285 refetchOnWindowFocus: false,286 });287}288289//UPDATE hook (put user in api)290function useUpdateUsers() {291 const queryClient = useQueryClient();292 return useMutation({293 mutationFn: async (users: User[]) => {294 //send api update request here295 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call296 return Promise.resolve();297 },298 //client side optimistic update299 onMutate: (newUsers: User[]) => {300 queryClient.setQueryData(301 ['users'],302 (prevUsers: any) =>303 prevUsers?.map((user: User) => {304 const newUser = newUsers.find((u) => u.id === user.id);305 return newUser ? newUser : user;306 }),307 );308 },309 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo310 });311}312313//DELETE hook (delete user in api)314function useDeleteUser() {315 const queryClient = useQueryClient();316 return useMutation({317 mutationFn: async (userId: string) => {318 //send api update request here319 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call320 return Promise.resolve();321 },322 //client side optimistic update323 onMutate: (userId: string) => {324 queryClient.setQueryData(325 ['users'],326 (prevUsers: any) =>327 prevUsers?.filter((user: User) => user.id !== userId),328 );329 },330 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo331 });332}333334const queryClient = new QueryClient();335336const ExampleWithProviders = () => (337 //Put this with your other react-query providers near root of your app338 <QueryClientProvider client={queryClient}>339 <Example />340 </QueryClientProvider>341);342343export default ExampleWithProviders;344345const validateRequired = (value: string) => !!value.length;346const validateEmail = (email: string) =>347 !!email.length &&348 email349 .toLowerCase()350 .match(351 /^(([^<>()[\]\\.,;:\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,}))$/,352 );353354function validateUser(user: User) {355 return {356 firstName: !validateRequired(user.firstName)357 ? 'First Name is Required'358 : '',359 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',360 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',361 };362}363
View Extra Storybook Examples