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 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