MRT logoMaterial React Table

Editing (CRUD) Inline Table 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 "table" editing mode, which allows you to edit a single cell at a time, but all rows are always in an open editing state. Hook up your own event listeners to save the data to your backend.

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

View Extra Storybook Examples