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.
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 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 Tooltip,19} from '@mui/material';20import {21 QueryClient,22 QueryClientProvider,23 useMutation,24 useQuery,25 useQueryClient,26} from '@tanstack/react-query';27import { type User, fakeData, usStates } from './makeData';28import EditIcon from '@mui/icons-material/Edit';29import DeleteIcon from '@mui/icons-material/Delete';3031const Example = () => {32 const [validationErrors, setValidationErrors] = useState<33 Record<string, string | undefined>34 >({});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: {48 type: 'email',49 required: true,50 error: !!validationErrors?.firstName,51 helperText: validationErrors?.firstName,52 //remove any previous validation errors when user focuses on the input53 onFocus: () =>54 setValidationErrors({55 ...validationErrors,56 firstName: undefined,57 }),58 //optionally add validation checking for onBlur or onChange59 },60 },61 {62 accessorKey: 'lastName',63 header: 'Last Name',64 muiEditTextFieldProps: {65 type: 'email',66 required: true,67 error: !!validationErrors?.lastName,68 helperText: validationErrors?.lastName,69 //remove any previous validation errors when user focuses on the input70 onFocus: () =>71 setValidationErrors({72 ...validationErrors,73 lastName: undefined,74 }),75 },76 },77 {78 accessorKey: 'email',79 header: 'Email',80 muiEditTextFieldProps: {81 type: 'email',82 required: true,83 error: !!validationErrors?.email,84 helperText: validationErrors?.email,85 //remove any previous validation errors when user focuses on the input86 onFocus: () =>87 setValidationErrors({88 ...validationErrors,89 email: undefined,90 }),91 },92 },93 {94 accessorKey: 'state',95 header: 'State',96 editVariant: 'select',97 editSelectOptions: usStates,98 muiEditTextFieldProps: {99 select: true,100 error: !!validationErrors?.state,101 helperText: validationErrors?.state,102 },103 },104 ],105 [validationErrors],106 );107108 //call CREATE hook109 const { mutateAsync: createUser, isPending: isCreatingUser } =110 useCreateUser();111 //call READ hook112 const {113 data: fetchedUsers = [],114 isError: isLoadingUsersError,115 isFetching: isFetchingUsers,116 isLoading: isLoadingUsers,117 } = useGetUsers();118 //call UPDATE hook119 const { mutateAsync: updateUser, isPending: isUpdatingUser } =120 useUpdateUser();121 //call DELETE hook122 const { mutateAsync: deleteUser, isPending: isDeletingUser } =123 useDeleteUser();124125 //CREATE action126 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({127 values,128 table,129 }) => {130 const newValidationErrors = validateUser(values);131 if (Object.values(newValidationErrors).some((error) => error)) {132 setValidationErrors(newValidationErrors);133 return;134 }135 setValidationErrors({});136 await createUser(values);137 table.setCreatingRow(null); //exit creating mode138 };139140 //UPDATE action141 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({142 values,143 table,144 }) => {145 const newValidationErrors = validateUser(values);146 if (Object.values(newValidationErrors).some((error) => error)) {147 setValidationErrors(newValidationErrors);148 return;149 }150 setValidationErrors({});151 await updateUser(values);152 table.setEditingRow(null); //exit editing mode153 };154155 //DELETE action156 const openDeleteConfirmModal = (row: MRT_Row<User>) => {157 if (window.confirm('Are you sure you want to delete this user?')) {158 deleteUser(row.original.id);159 }160 };161162 const table = useMaterialReactTable({163 columns,164 data: fetchedUsers,165 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)166 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)167 enableEditing: true,168 getRowId: (row) => row.id,169 muiToolbarAlertBannerProps: isLoadingUsersError170 ? {171 color: 'error',172 children: 'Error loading data',173 }174 : undefined,175 muiTableContainerProps: {176 sx: {177 minHeight: '500px',178 },179 },180 onCreatingRowCancel: () => setValidationErrors({}),181 onCreatingRowSave: handleCreateUser,182 onEditingRowCancel: () => setValidationErrors({}),183 onEditingRowSave: handleSaveUser,184 //optionally customize modal content185 renderCreateRowDialogContent: ({ table, row, internalEditComponents }) => (186 <>187 <DialogTitle variant="h3">Create New User</DialogTitle>188 <DialogContent189 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}190 >191 {internalEditComponents} {/* or render custom edit components here */}192 </DialogContent>193 <DialogActions>194 <MRT_EditActionButtons variant="text" table={table} row={row} />195 </DialogActions>196 </>197 ),198 //optionally customize modal content199 renderEditRowDialogContent: ({ table, row, internalEditComponents }) => (200 <>201 <DialogTitle variant="h3">Edit User</DialogTitle>202 <DialogContent203 sx={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}204 >205 {internalEditComponents} {/* or render custom edit components here */}206 </DialogContent>207 <DialogActions>208 <MRT_EditActionButtons variant="text" table={table} row={row} />209 </DialogActions>210 </>211 ),212 renderRowActions: ({ row, table }) => (213 <Box sx={{ display: 'flex', gap: '1rem' }}>214 <Tooltip title="Edit">215 <IconButton onClick={() => table.setEditingRow(row)}>216 <EditIcon />217 </IconButton>218 </Tooltip>219 <Tooltip title="Delete">220 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>221 <DeleteIcon />222 </IconButton>223 </Tooltip>224 </Box>225 ),226 renderTopToolbarCustomActions: ({ table }) => (227 <Button228 variant="contained"229 onClick={() => {230 table.setCreatingRow(true); //simplest way to open the create row modal with no default values231 //or you can pass in a row object to set default values with the `createRow` helper function232 // table.setCreatingRow(233 // createRow(table, {234 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios235 // }),236 // );237 }}238 >239 Create New User240 </Button>241 ),242 state: {243 isLoading: isLoadingUsers,244 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,245 showAlertBanner: isLoadingUsersError,246 showProgressBars: isFetchingUsers,247 },248 });249250 return <MaterialReactTable table={table} />;251};252253//CREATE hook (post new user to api)254function useCreateUser() {255 const queryClient = useQueryClient();256 return useMutation({257 mutationFn: async (user: User) => {258 //send api update request here259 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call260 return Promise.resolve();261 },262 //client side optimistic update263 onMutate: (newUserInfo: User) => {264 queryClient.setQueryData(265 ['users'],266 (prevUsers: any) =>267 [268 ...prevUsers,269 {270 ...newUserInfo,271 id: (Math.random() + 1).toString(36).substring(7),272 },273 ] as User[],274 );275 },276 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo277 });278}279280//READ hook (get users from api)281function useGetUsers() {282 return useQuery<User[]>({283 queryKey: ['users'],284 queryFn: async () => {285 //send api request here286 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call287 return Promise.resolve(fakeData);288 },289 refetchOnWindowFocus: false,290 });291}292293//UPDATE hook (put user in api)294function useUpdateUser() {295 const queryClient = useQueryClient();296 return useMutation({297 mutationFn: async (user: User) => {298 //send api update request here299 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call300 return Promise.resolve();301 },302 //client side optimistic update303 onMutate: (newUserInfo: User) => {304 queryClient.setQueryData(305 ['users'],306 (prevUsers: any) =>307 prevUsers?.map((prevUser: User) =>308 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,309 ),310 );311 },312 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo313 });314}315316//DELETE hook (delete user in api)317function useDeleteUser() {318 const queryClient = useQueryClient();319 return useMutation({320 mutationFn: async (userId: string) => {321 //send api update request here322 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call323 return Promise.resolve();324 },325 //client side optimistic update326 onMutate: (userId: string) => {327 queryClient.setQueryData(328 ['users'],329 (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