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

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