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.

More Examples

Demo

Open StackblitzOpen Code SandboxOpen on GitHub

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

View Extra Storybook Examples