MRT logoMaterial React Table

Editing (CRUD) Inline Row 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 "row" editing mode, which allows you to edit a single row at a time with built-in save and cancel buttons.

Check out the other editing modes down below, and the editing guide for more information.

Non TanStack Query Fetching
More Examples
1-10 of 10

Source Code

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 { Box, Button, IconButton, Tooltip } from '@mui/material';
11import {
12 QueryClient,
13 QueryClientProvider,
14 useMutation,
15 useQuery,
16 useQueryClient,
17} from '@tanstack/react-query';
18import { type User, fakeData, usStates } from './makeData';
19import EditIcon from '@mui/icons-material/Edit';
20import DeleteIcon from '@mui/icons-material/Delete';
21
22const Example = () => {
23 const [validationErrors, setValidationErrors] = useState<
24 Record<string, string | undefined>
25 >({});
26
27 const columns = useMemo<MRT_ColumnDef<User>[]>(
28 () => [
29 {
30 accessorKey: 'id',
31 header: 'Id',
32 enableEditing: false,
33 size: 80,
34 },
35 {
36 accessorKey: 'firstName',
37 header: 'First Name',
38 muiEditTextFieldProps: {
39 required: true,
40 error: !!validationErrors?.firstName,
41 helperText: validationErrors?.firstName,
42 //remove any previous validation errors when user focuses on the input
43 onFocus: () =>
44 setValidationErrors({
45 ...validationErrors,
46 firstName: undefined,
47 }),
48 //optionally add validation checking for onBlur or onChange
49 },
50 },
51 {
52 accessorKey: 'lastName',
53 header: 'Last Name',
54 muiEditTextFieldProps: {
55 required: true,
56 error: !!validationErrors?.lastName,
57 helperText: validationErrors?.lastName,
58 //remove any previous validation errors when user focuses on the input
59 onFocus: () =>
60 setValidationErrors({
61 ...validationErrors,
62 lastName: undefined,
63 }),
64 },
65 },
66 {
67 accessorKey: 'email',
68 header: 'Email',
69 muiEditTextFieldProps: {
70 type: 'email',
71 required: true,
72 error: !!validationErrors?.email,
73 helperText: validationErrors?.email,
74 //remove any previous validation errors when user focuses on the input
75 onFocus: () =>
76 setValidationErrors({
77 ...validationErrors,
78 email: undefined,
79 }),
80 },
81 },
82 {
83 accessorKey: 'state',
84 header: 'State',
85 editVariant: 'select',
86 editSelectOptions: usStates,
87 muiEditTextFieldProps: {
88 select: true,
89 error: !!validationErrors?.state,
90 helperText: validationErrors?.state,
91 },
92 },
93 ],
94 [validationErrors],
95 );
96
97 //call CREATE hook
98 const { mutateAsync: createUser, isPending: isCreatingUser } =
99 useCreateUser();
100 //call READ hook
101 const {
102 data: fetchedUsers = [],
103 isError: isLoadingUsersError,
104 isFetching: isFetchingUsers,
105 isLoading: isLoadingUsers,
106 } = useGetUsers();
107 //call UPDATE hook
108 const { mutateAsync: updateUser, isPending: isUpdatingUser } =
109 useUpdateUser();
110 //call DELETE hook
111 const { mutateAsync: deleteUser, isPending: isDeletingUser } =
112 useDeleteUser();
113
114 //CREATE action
115 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
116 values,
117 table,
118 }) => {
119 const newValidationErrors = validateUser(values);
120 if (Object.values(newValidationErrors).some((error) => error)) {
121 setValidationErrors(newValidationErrors);
122 return;
123 }
124 setValidationErrors({});
125 await createUser(values);
126 table.setCreatingRow(null); //exit creating mode
127 };
128
129 //UPDATE action
130 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({
131 values,
132 table,
133 }) => {
134 const newValidationErrors = validateUser(values);
135 if (Object.values(newValidationErrors).some((error) => error)) {
136 setValidationErrors(newValidationErrors);
137 return;
138 }
139 setValidationErrors({});
140 await updateUser(values);
141 table.setEditingRow(null); //exit editing mode
142 };
143
144 //DELETE action
145 const openDeleteConfirmModal = (row: MRT_Row<User>) => {
146 if (window.confirm('Are you sure you want to delete this user?')) {
147 deleteUser(row.original.id);
148 }
149 };
150
151 const table = useMaterialReactTable({
152 columns,
153 data: fetchedUsers,
154 createDisplayMode: 'row', // ('modal', and 'custom' are also available)
155 editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)
156 enableEditing: true,
157 getRowId: (row) => row.id,
158 muiToolbarAlertBannerProps: isLoadingUsersError
159 ? {
160 color: 'error',
161 children: 'Error loading data',
162 }
163 : undefined,
164 muiTableContainerProps: {
165 sx: {
166 minHeight: '500px',
167 },
168 },
169 onCreatingRowCancel: () => setValidationErrors({}),
170 onCreatingRowSave: handleCreateUser,
171 onEditingRowCancel: () => setValidationErrors({}),
172 onEditingRowSave: handleSaveUser,
173 renderRowActions: ({ row, table }) => (
174 <Box sx={{ display: 'flex', gap: '1rem' }}>
175 <Tooltip title="Edit">
176 <IconButton onClick={() => table.setEditingRow(row)}>
177 <EditIcon />
178 </IconButton>
179 </Tooltip>
180 <Tooltip title="Delete">
181 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
182 <DeleteIcon />
183 </IconButton>
184 </Tooltip>
185 </Box>
186 ),
187 renderTopToolbarCustomActions: ({ table }) => (
188 <Button
189 variant="contained"
190 onClick={() => {
191 table.setCreatingRow(true); //simplest way to open the create row modal with no default values
192 //or you can pass in a row object to set default values with the `createRow` helper function
193 // table.setCreatingRow(
194 // createRow(table, {
195 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios
196 // }),
197 // );
198 }}
199 >
200 Create New User
201 </Button>
202 ),
203 state: {
204 isLoading: isLoadingUsers,
205 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
206 showAlertBanner: isLoadingUsersError,
207 showProgressBars: isFetchingUsers,
208 },
209 });
210
211 return <MaterialReactTable table={table} />;
212};
213
214//CREATE hook (post new user to api)
215function useCreateUser() {
216 const queryClient = useQueryClient();
217 return useMutation({
218 mutationFn: async (user: User) => {
219 //send api update request here
220 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
221 return Promise.resolve();
222 },
223 //client side optimistic update
224 onMutate: (newUserInfo: User) => {
225 queryClient.setQueryData(
226 ['users'],
227 (prevUsers: any) =>
228 [
229 ...prevUsers,
230 {
231 ...newUserInfo,
232 id: (Math.random() + 1).toString(36).substring(7),
233 },
234 ] as User[],
235 );
236 },
237 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
238 });
239}
240
241//READ hook (get users from api)
242function useGetUsers() {
243 return useQuery<User[]>({
244 queryKey: ['users'],
245 queryFn: async () => {
246 //send api request here
247 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
248 return Promise.resolve(fakeData);
249 },
250 refetchOnWindowFocus: false,
251 });
252}
253
254//UPDATE hook (put user in api)
255function useUpdateUser() {
256 const queryClient = useQueryClient();
257 return useMutation({
258 mutationFn: async (user: User) => {
259 //send api update request here
260 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
261 return Promise.resolve();
262 },
263 //client side optimistic update
264 onMutate: (newUserInfo: User) => {
265 queryClient.setQueryData(['users'], (prevUsers: any) =>
266 prevUsers?.map((prevUser: User) =>
267 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,
268 ),
269 );
270 },
271 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
272 });
273}
274
275//DELETE hook (delete user in api)
276function useDeleteUser() {
277 const queryClient = useQueryClient();
278 return useMutation({
279 mutationFn: async (userId: string) => {
280 //send api update request here
281 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
282 return Promise.resolve();
283 },
284 //client side optimistic update
285 onMutate: (userId: string) => {
286 queryClient.setQueryData(['users'], (prevUsers: any) =>
287 prevUsers?.filter((user: User) => user.id !== userId),
288 );
289 },
290 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
291 });
292}
293
294const queryClient = new QueryClient();
295
296const ExampleWithProviders = () => (
297 //Put this with your other react-query providers near root of your app
298 <QueryClientProvider client={queryClient}>
299 <Example />
300 </QueryClientProvider>
301);
302
303export default ExampleWithProviders;
304
305const validateRequired = (value: string) => !!value.length;
306const validateEmail = (email: string) =>
307 !!email.length &&
308 email
309 .toLowerCase()
310 .match(
311 /^(([^<>()[\]\\.,;:\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,}))$/,
312 );
313
314function validateUser(user: User) {
315 return {
316 firstName: !validateRequired(user.firstName)
317 ? 'First Name is Required'
318 : '',
319 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
320 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
321 };
322}
323

View Extra Storybook Examples