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 enableCellActions: true,179 enableClickToCopy: 'context-menu',180 enableColumnPinning: true,181 enableEditing: true,182 enableRowActions: true,183 getRowId: (row) => row.id,184 muiToolbarAlertBannerProps: isLoadingUsersError185 ? {186 color: 'error',187 children: 'Error loading data',188 }189 : undefined,190 muiTableContainerProps: {191 sx: {192 minHeight: '500px',193 },194 },195 onCreatingRowCancel: () => setValidationErrors({}),196 onCreatingRowSave: handleCreateUser,197 renderRowActions: ({ row }) => (198 <Box sx={{ display: 'flex', gap: '1rem' }}>199 <Tooltip title="Delete">200 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>201 <DeleteIcon />202 </IconButton>203 </Tooltip>204 </Box>205 ),206 renderBottomToolbarCustomActions: () => (207 <Box sx={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>208 <Button209 color="success"210 variant="contained"211 onClick={handleSaveUsers}212 disabled={213 Object.keys(editedUsers).length === 0 ||214 Object.values(validationErrors).some((error) => !!error)215 }216 >217 {isUpdatingUsers ? <CircularProgress size={25} /> : 'Save'}218 </Button>219 {Object.values(validationErrors).some((error) => !!error) && (220 <Typography color="error">Fix errors before submitting</Typography>221 )}222 </Box>223 ),224 renderTopToolbarCustomActions: ({ table }) => (225 <Button226 variant="contained"227 onClick={() => {228 table.setCreatingRow(true); //simplest way to open the create row modal with no default values229 //or you can pass in a row object to set default values with the `createRow` helper function230 // table.setCreatingRow(231 // createRow(table, {232 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios233 // }),234 // );235 }}236 >237 Create New User238 </Button>239 ),240 initialState: {241 columnPinning: {242 right: ['mrt-row-actions'],243 },244 },245 state: {246 isLoading: isLoadingUsers,247 isSaving: isCreatingUser || isUpdatingUsers || isDeletingUser,248 showAlertBanner: isLoadingUsersError,249 showProgressBars: isFetchingUsers,250 },251 });252253 return <MaterialReactTable table={table} />;254};255256//CREATE hook (post new user to api)257function useCreateUser() {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 [271 ...prevUsers,272 {273 ...newUserInfo,274 id: (Math.random() + 1).toString(36).substring(7),275 },276 ] as User[],277 );278 },279 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo280 });281}282283//READ hook (get users from api)284function useGetUsers() {285 return useQuery<User[]>({286 queryKey: ['users'],287 queryFn: async () => {288 //send api request here289 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call290 return Promise.resolve(fakeData);291 },292 refetchOnWindowFocus: false,293 });294}295296//UPDATE hook (put user in api)297function useUpdateUsers() {298 const queryClient = useQueryClient();299 return useMutation({300 mutationFn: async (users: User[]) => {301 //send api update request here302 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call303 return Promise.resolve();304 },305 //client side optimistic update306 onMutate: (newUsers: User[]) => {307 queryClient.setQueryData(['users'], (prevUsers: any) =>308 prevUsers?.map((user: User) => {309 const newUser = newUsers.find((u) => u.id === user.id);310 return newUser ? newUser : user;311 }),312 );313 },314 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo315 });316}317318//DELETE hook (delete user in api)319function useDeleteUser() {320 const queryClient = useQueryClient();321 return useMutation({322 mutationFn: async (userId: string) => {323 //send api update request here324 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call325 return Promise.resolve();326 },327 //client side optimistic update328 onMutate: (userId: string) => {329 queryClient.setQueryData(['users'], (prevUsers: any) =>330 prevUsers?.filter((user: User) => user.id !== userId),331 );332 },333 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo334 });335}336337const queryClient = new QueryClient();338339const ExampleWithProviders = () => (340 //Put this with your other react-query providers near root of your app341 <QueryClientProvider client={queryClient}>342 <Example />343 </QueryClientProvider>344);345346export default ExampleWithProviders;347348const validateRequired = (value: string) => !!value.length;349const validateEmail = (email: string) =>350 !!email.length &&351 email352 .toLowerCase()353 .match(354 /^(([^<>()[\]\\.,;:\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,}))$/,355 );356357function validateUser(user: User) {358 return {359 firstName: !validateRequired(user.firstName)360 ? 'First Name is Required'361 : '',362 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',363 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',364 };365}366
View Extra Storybook Examples