Editing (CRUD) Inline Table 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 "table"
editing mode, which allows you to edit a single cell at a time, but all rows are always in an open editing state. 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
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 {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: 'table', // ('modal', 'row', 'cell', and 'custom' are also178 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