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
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 required: true,49 error: !!validationErrors?.firstName,50 helperText: validationErrors?.firstName,51 //remove any previous validation errors when user focuses on the input52 onFocus: () =>53 setValidationErrors({54 ...validationErrors,55 firstName: undefined,56 }),57 //optionally add validation checking for onBlur or onChange58 },59 },60 {61 accessorKey: 'lastName',62 header: 'Last Name',63 muiEditTextFieldProps: {64 required: true,65 error: !!validationErrors?.lastName,66 helperText: validationErrors?.lastName,67 //remove any previous validation errors when user focuses on the input68 onFocus: () =>69 setValidationErrors({70 ...validationErrors,71 lastName: undefined,72 }),73 },74 },75 {76 accessorKey: 'email',77 header: 'Email',78 muiEditTextFieldProps: {79 type: 'email',80 required: true,81 error: !!validationErrors?.email,82 helperText: validationErrors?.email,83 //remove any previous validation errors when user focuses on the input84 onFocus: () =>85 setValidationErrors({86 ...validationErrors,87 email: undefined,88 }),89 },90 },91 {92 accessorKey: 'state',93 header: 'State',94 editVariant: 'select',95 editSelectOptions: usStates,96 muiEditTextFieldProps: {97 select: true,98 error: !!validationErrors?.state,99 helperText: validationErrors?.state,100 },101 },102 ],103 [validationErrors],104 );105106 //call CREATE hook107 const { mutateAsync: createUser, isPending: isCreatingUser } =108 useCreateUser();109 //call READ hook110 const {111 data: fetchedUsers = [],112 isError: isLoadingUsersError,113 isFetching: isFetchingUsers,114 isLoading: isLoadingUsers,115 } = useGetUsers();116 //call UPDATE hook117 const { mutateAsync: updateUser, isPending: isUpdatingUser } =118 useUpdateUser();119 //call DELETE hook120 const { mutateAsync: deleteUser, isPending: isDeletingUser } =121 useDeleteUser();122123 //CREATE action124 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({125 values,126 table,127 }) => {128 const newValidationErrors = validateUser(values);129 if (Object.values(newValidationErrors).some((error) => error)) {130 setValidationErrors(newValidationErrors);131 return;132 }133 setValidationErrors({});134 await createUser(values);135 table.setCreatingRow(null); //exit creating mode136 };137138 //UPDATE action139 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({140 values,141 table,142 }) => {143 const newValidationErrors = validateUser(values);144 if (Object.values(newValidationErrors).some((error) => error)) {145 setValidationErrors(newValidationErrors);146 return;147 }148 setValidationErrors({});149 await updateUser(values);150 table.setEditingRow(null); //exit editing mode151 };152153 //DELETE action154 const openDeleteConfirmModal = (row: MRT_Row<User>) => {155 if (window.confirm('Are you sure you want to delete this user?')) {156 deleteUser(row.original.id);157 }158 };159160 const table = useMaterialReactTable({161 columns,162 data: fetchedUsers,163 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)164 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)165 enableEditing: true,166 getRowId: (row) => row.id,167 muiToolbarAlertBannerProps: isLoadingUsersError168 ? {169 color: 'error',170 children: 'Error loading data',171 }172 : undefined,173 muiTableContainerProps: {174 sx: {175 minHeight: '500px',176 },177 },178 onCreatingRowCancel: () => setValidationErrors({}),179 onCreatingRowSave: handleCreateUser,180 onEditingRowCancel: () => setValidationErrors({}),181 onEditingRowSave: handleSaveUser,182 //optionally customize modal content183 renderCreateRowDialogContent: ({ table, row, internalEditComponents }) => (184 <>185 <DialogTitle variant="h3">Create New User</DialogTitle>186 <DialogContent187 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}188 >189 {internalEditComponents} {/* or render custom edit components here */}190 </DialogContent>191 <DialogActions>192 <MRT_EditActionButtons variant="text" table={table} row={row} />193 </DialogActions>194 </>195 ),196 //optionally customize modal content197 renderEditRowDialogContent: ({ table, row, internalEditComponents }) => (198 <>199 <DialogTitle variant="h3">Edit User</DialogTitle>200 <DialogContent201 sx={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}202 >203 {internalEditComponents} {/* or render custom edit components here */}204 </DialogContent>205 <DialogActions>206 <MRT_EditActionButtons variant="text" table={table} row={row} />207 </DialogActions>208 </>209 ),210 renderRowActions: ({ row, table }) => (211 <Box sx={{ display: 'flex', gap: '1rem' }}>212 <Tooltip title="Edit">213 <IconButton onClick={() => table.setEditingRow(row)}>214 <EditIcon />215 </IconButton>216 </Tooltip>217 <Tooltip title="Delete">218 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>219 <DeleteIcon />220 </IconButton>221 </Tooltip>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 state: {241 isLoading: isLoadingUsers,242 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,243 showAlertBanner: isLoadingUsersError,244 showProgressBars: isFetchingUsers,245 },246 });247248 return <MaterialReactTable table={table} />;249};250251//CREATE hook (post new user to api)252function useCreateUser() {253 const queryClient = useQueryClient();254 return useMutation({255 mutationFn: async (user: User) => {256 //send api update request here257 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call258 return Promise.resolve();259 },260 //client side optimistic update261 onMutate: (newUserInfo: User) => {262 queryClient.setQueryData(263 ['users'],264 (prevUsers: any) =>265 [266 ...prevUsers,267 {268 ...newUserInfo,269 id: (Math.random() + 1).toString(36).substring(7),270 },271 ] as User[],272 );273 },274 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo275 });276}277278//READ hook (get users from api)279function useGetUsers() {280 return useQuery<User[]>({281 queryKey: ['users'],282 queryFn: async () => {283 //send api request here284 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call285 return Promise.resolve(fakeData);286 },287 refetchOnWindowFocus: false,288 });289}290291//UPDATE hook (put user in api)292function useUpdateUser() {293 const queryClient = useQueryClient();294 return useMutation({295 mutationFn: async (user: User) => {296 //send api update request here297 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call298 return Promise.resolve();299 },300 //client side optimistic update301 onMutate: (newUserInfo: User) => {302 queryClient.setQueryData(['users'], (prevUsers: any) =>303 prevUsers?.map((prevUser: User) =>304 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,305 ),306 );307 },308 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo309 });310}311312//DELETE hook (delete user in api)313function useDeleteUser() {314 const queryClient = useQueryClient();315 return useMutation({316 mutationFn: async (userId: string) => {317 //send api update request here318 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call319 return Promise.resolve();320 },321 //client side optimistic update322 onMutate: (userId: string) => {323 queryClient.setQueryData(['users'], (prevUsers: any) =>324 prevUsers?.filter((user: User) => user.id !== userId),325 );326 },327 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo328 });329}330331const queryClient = new QueryClient();332333const ExampleWithProviders = () => (334 //Put this with your other react-query providers near root of your app335 <QueryClientProvider client={queryClient}>336 <Example />337 </QueryClientProvider>338);339340export default ExampleWithProviders;341342const validateRequired = (value: string) => !!value.length;343const validateEmail = (email: string) =>344 !!email.length &&345 email346 .toLowerCase()347 .match(348 /^(([^<>()[\]\\.,;:\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,}))$/,349 );350351function validateUser(user: User) {352 return {353 firstName: !validateRequired(user.firstName)354 ? 'First Name is Required'355 : '',356 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',357 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',358 };359}360
View Extra Storybook Examples