Editing (CRUD) 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 default "modal"
editing mode, where a dialog opens up to edit 1 row at a time. However be sure to check out the other editing modes and see if they fit your use case better. Other editing modes include "row"
(1 row inline at a time), "cell"
(1 cell inline at time), and "table"
(all cells always editable).
Actions | Id | First Name | Last Name | Email | State |
---|---|---|---|---|---|
1import { useMemo, useState } from 'react';2import {3 MRT_EditActionButtons,4 MaterialReactTable,5 // createRow,6 type MRT_ColumnDef,7 type MRT_Row,8 type MRT_TableOptions,9 useMaterialReactTable,10} from 'material-react-table';11import {12 Box,13 Button,14 DialogActions,15 DialogContent,16 DialogTitle,17 IconButton,18 Stack,19 Tooltip,20 Typography,21} from '@mui/material';22import {23 QueryClient,24 QueryClientProvider,25 useMutation,26 useQuery,27 useQueryClient,28} from '@tanstack/react-query';29import { type User, fakeData, usStates } from './makeData';30import EditIcon from '@mui/icons-material/Edit';31import DeleteIcon from '@mui/icons-material/Delete';3233const Example = () => {34 const [validationErrors, setValidationErrors] = useState<35 Record<string, string | undefined>36 >({});3738 const columns = useMemo<MRT_ColumnDef<User>[]>(39 () => [40 {41 accessorKey: 'id',42 header: 'Id',43 enableEditing: false,44 size: 80,45 },46 {47 accessorKey: 'firstName',48 header: 'First Name',49 muiEditTextFieldProps: {50 type: 'email',51 required: true,52 error: !!validationErrors?.firstName,53 helperText: validationErrors?.firstName,54 //remove any previous validation errors when user focuses on the input55 onFocus: () =>56 setValidationErrors({57 ...validationErrors,58 firstName: undefined,59 }),60 //optionally add validation checking for onBlur or onChange61 },62 },63 {64 accessorKey: 'lastName',65 header: 'Last Name',66 muiEditTextFieldProps: {67 type: 'email',68 required: true,69 error: !!validationErrors?.lastName,70 helperText: validationErrors?.lastName,71 //remove any previous validation errors when user focuses on the input72 onFocus: () =>73 setValidationErrors({74 ...validationErrors,75 lastName: undefined,76 }),77 },78 },79 {80 accessorKey: 'email',81 header: 'Email',82 muiEditTextFieldProps: {83 type: 'email',84 required: true,85 error: !!validationErrors?.email,86 helperText: validationErrors?.email,87 //remove any previous validation errors when user focuses on the input88 onFocus: () =>89 setValidationErrors({90 ...validationErrors,91 email: undefined,92 }),93 },94 },95 {96 accessorKey: 'state',97 header: 'State',98 editVariant: 'select',99 editSelectOptions: usStates,100 muiEditTextFieldProps: {101 select: true,102 error: !!validationErrors?.state,103 helperText: validationErrors?.state,104 },105 },106 ],107 [validationErrors],108 );109110 //call CREATE hook111 const { mutateAsync: createUser, isLoading: isCreatingUser } =112 useCreateUser();113 //call READ hook114 const {115 data: fetchedUsers = [],116 isError: isLoadingUsersError,117 isFetching: isFetchingUsers,118 isLoading: isLoadingUsers,119 } = useGetUsers();120 //call UPDATE hook121 const { mutateAsync: updateUser, isLoading: isUpdatingUser } =122 useUpdateUser();123 //call DELETE hook124 const { mutateAsync: deleteUser, isLoading: isDeletingUser } =125 useDeleteUser();126127 //CREATE action128 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({129 values,130 table,131 }) => {132 const newValidationErrors = validateUser(values);133 if (Object.values(newValidationErrors).some((error) => error)) {134 setValidationErrors(newValidationErrors);135 return;136 }137 setValidationErrors({});138 await createUser(values);139 table.setCreatingRow(null); //exit creating mode140 };141142 //UPDATE action143 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({144 values,145 table,146 }) => {147 const newValidationErrors = validateUser(values);148 if (Object.values(newValidationErrors).some((error) => error)) {149 setValidationErrors(newValidationErrors);150 return;151 }152 setValidationErrors({});153 await updateUser(values);154 table.setEditingRow(null); //exit editing mode155 };156157 //DELETE action158 const openDeleteConfirmModal = (row: MRT_Row<User>) => {159 if (window.confirm('Are you sure you want to delete this user?')) {160 deleteUser(row.original.id);161 }162 };163164 const table = useMaterialReactTable({165 columns,166 data: fetchedUsers,167 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)168 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)169 enableEditing: true,170 getRowId: (row) => row.id,171 muiToolbarAlertBannerProps: isLoadingUsersError172 ? {173 color: 'error',174 children: 'Error loading data',175 }176 : undefined,177 muiTableContainerProps: {178 sx: {179 minHeight: '500px',180 },181 },182 onCreatingRowCancel: () => setValidationErrors({}),183 onCreatingRowSave: handleCreateUser,184 onEditingRowCancel: () => setValidationErrors({}),185 onEditingRowSave: handleSaveUser,186 //optionally customize modal content187 renderCreateRowModalContent: ({ table, row, internalEditComponents }) => (188 <>189 <DialogTitle variant="h3">Create New User</DialogTitle>190 <DialogContent191 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}192 >193 {internalEditComponents} {/* or render custom edit components here */}194 </DialogContent>195 <DialogActions>196 <MRT_EditActionButtons variant="text" table={table} row={row} />197 </DialogActions>198 </>199 ),200 //optionally customize modal content201 renderEditRowModalContent: ({ table, row, internalEditComponents }) => (202 <>203 <DialogTitle variant="h3">Edit User</DialogTitle>204 <DialogContent205 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}206 >207 {internalEditComponents} {/* or render custom edit components here */}208 </DialogContent>209 <DialogActions>210 <MRT_EditActionButtons variant="text" table={table} row={row} />211 </DialogActions>212 </>213 ),214 renderRowActions: ({ row, table }) => (215 <Box sx={{ display: 'flex', gap: '1rem' }}>216 <Tooltip title="Edit">217 <IconButton onClick={() => table.setEditingRow(row)}>218 <EditIcon />219 </IconButton>220 </Tooltip>221 <Tooltip title="Delete">222 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>223 <DeleteIcon />224 </IconButton>225 </Tooltip>226 </Box>227 ),228 renderTopToolbarCustomActions: ({ table }) => (229 <Button230 variant="contained"231 onClick={() => {232 table.setCreatingRow(true); //simplest way to open the create row modal with no default values233 //or you can pass in a row object to set default values with the `createRow` helper function234 // table.setCreatingRow(235 // createRow(table, {236 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios237 // }),238 // );239 }}240 >241 Create New User242 </Button>243 ),244 state: {245 isLoading: isLoadingUsers,246 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,247 showAlertBanner: isLoadingUsersError,248 showProgressBars: isFetchingUsers,249 },250 });251252 return <MaterialReactTable table={table} />;253};254255//CREATE hook (post new user to api)256function useCreateUser() {257 const queryClient = useQueryClient();258 return useMutation({259 mutationFn: async (user: User) => {260 //send api update request here261 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call262 return Promise.resolve();263 },264 //client side optimistic update265 onMutate: (newUserInfo: User) => {266 queryClient.setQueryData(267 ['users'],268 (prevUsers: any) =>269 [270 ...prevUsers,271 {272 ...newUserInfo,273 id: (Math.random() + 1).toString(36).substring(7),274 },275 ] as User[],276 );277 },278 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo279 });280}281282//READ hook (get users from api)283function useGetUsers() {284 return useQuery<User[]>({285 queryKey: ['users'],286 queryFn: async () => {287 //send api request here288 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call289 return Promise.resolve(fakeData);290 },291 refetchOnWindowFocus: false,292 });293}294295//UPDATE hook (put user in api)296function useUpdateUser() {297 const queryClient = useQueryClient();298 return useMutation({299 mutationFn: async (user: User) => {300 //send api update request here301 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call302 return Promise.resolve();303 },304 //client side optimistic update305 onMutate: (newUserInfo: User) => {306 queryClient.setQueryData(307 ['users'],308 (prevUsers: any) =>309 prevUsers?.map((prevUser: User) =>310 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,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(330 ['users'],331 (prevUsers: any) =>332 prevUsers?.filter((user: User) => user.id !== userId),333 );334 },335 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo336 });337}338339const queryClient = new QueryClient();340341const ExampleWithProviders = () => (342 //Put this with your other react-query providers near root of your app343 <QueryClientProvider client={queryClient}>344 <Example />345 </QueryClientProvider>346);347348export default ExampleWithProviders;349350const validateRequired = (value: string) => !!value.length;351const validateEmail = (email: string) =>352 !!email.length &&353 email354 .toLowerCase()355 .match(356 /^(([^<>()[\]\\.,;:\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,}))$/,357 );358359function validateUser(user: User) {360 return {361 firstName: !validateRequired(user.firstName)362 ? 'First Name is Required'363 : '',364 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',365 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',366 };367}368
View Extra Storybook Examples