MRT logoMaterial React Table

Editing (CRUD) Inline Cell 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 "cell" editing mode, which allows you to edit a single cell at a time. 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: 'cell', // ('modal', 'row', 'table', and 'custom' are also available)
178 enableCellActions: true,
179 enableClickToCopy: 'context-menu',
180 enableColumnPinning: true,
181 enableEditing: true,
182 enableRowActions: true,
183 getRowId: (row) => row.id,
184 muiToolbarAlertBannerProps: isLoadingUsersError
185 ? {
186 color: 'error',
187 children: 'Error loading data',
188 }
189 : undefined,
190 muiTableContainerProps: {
191 sx: {
192 minHeight: '500px',
193 },
194 },
195 onCreatingRowCancel: () => setValidationErrors({}),
196 onCreatingRowSave: handleCreateUser,
197 renderRowActions: ({ row }) => (
198 <Box sx={{ display: 'flex', gap: '1rem' }}>
199 <Tooltip title="Delete">
200 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
201 <DeleteIcon />
202 </IconButton>
203 </Tooltip>
204 </Box>
205 ),
206 renderBottomToolbarCustomActions: () => (
207 <Box sx={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
208 <Button
209 color="success"
210 variant="contained"
211 onClick={handleSaveUsers}
212 disabled={
213 Object.keys(editedUsers).length === 0 ||
214 Object.values(validationErrors).some((error) => !!error)
215 }
216 >
217 {isUpdatingUsers ? <CircularProgress size={25} /> : 'Save'}
218 </Button>
219 {Object.values(validationErrors).some((error) => !!error) && (
220 <Typography color="error">Fix errors before submitting</Typography>
221 )}
222 </Box>
223 ),
224 renderTopToolbarCustomActions: ({ table }) => (
225 <Button
226 variant="contained"
227 onClick={() => {
228 table.setCreatingRow(true); //simplest way to open the create row modal with no default values
229 //or you can pass in a row object to set default values with the `createRow` helper function
230 // table.setCreatingRow(
231 // createRow(table, {
232 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios
233 // }),
234 // );
235 }}
236 >
237 Create New User
238 </Button>
239 ),
240 initialState: {
241 columnPinning: {
242 right: ['mrt-row-actions'],
243 },
244 },
245 state: {
246 isLoading: isLoadingUsers,
247 isSaving: isCreatingUser || isUpdatingUsers || isDeletingUser,
248 showAlertBanner: isLoadingUsersError,
249 showProgressBars: isFetchingUsers,
250 },
251 });
252
253 return <MaterialReactTable table={table} />;
254};
255
256//CREATE hook (post new user to api)
257function useCreateUser() {
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 [
271 ...prevUsers,
272 {
273 ...newUserInfo,
274 id: (Math.random() + 1).toString(36).substring(7),
275 },
276 ] as User[],
277 );
278 },
279 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
280 });
281}
282
283//READ hook (get users from api)
284function useGetUsers() {
285 return useQuery<User[]>({
286 queryKey: ['users'],
287 queryFn: async () => {
288 //send api request here
289 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
290 return Promise.resolve(fakeData);
291 },
292 refetchOnWindowFocus: false,
293 });
294}
295
296//UPDATE hook (put user in api)
297function useUpdateUsers() {
298 const queryClient = useQueryClient();
299 return useMutation({
300 mutationFn: async (users: User[]) => {
301 //send api update request here
302 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
303 return Promise.resolve();
304 },
305 //client side optimistic update
306 onMutate: (newUsers: User[]) => {
307 queryClient.setQueryData(['users'], (prevUsers: any) =>
308 prevUsers?.map((user: User) => {
309 const newUser = newUsers.find((u) => u.id === user.id);
310 return newUser ? newUser : user;
311 }),
312 );
313 },
314 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
315 });
316}
317
318//DELETE hook (delete user in api)
319function useDeleteUser() {
320 const queryClient = useQueryClient();
321 return useMutation({
322 mutationFn: async (userId: string) => {
323 //send api update request here
324 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
325 return Promise.resolve();
326 },
327 //client side optimistic update
328 onMutate: (userId: string) => {
329 queryClient.setQueryData(['users'], (prevUsers: any) =>
330 prevUsers?.filter((user: User) => user.id !== userId),
331 );
332 },
333 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
334 });
335}
336
337const queryClient = new QueryClient();
338
339const ExampleWithProviders = () => (
340 //Put this with your other react-query providers near root of your app
341 <QueryClientProvider client={queryClient}>
342 <Example />
343 </QueryClientProvider>
344);
345
346export default ExampleWithProviders;
347
348const validateRequired = (value: string) => !!value.length;
349const validateEmail = (email: string) =>
350 !!email.length &&
351 email
352 .toLowerCase()
353 .match(
354 /^(([^<>()[\]\\.,;:\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,}))$/,
355 );
356
357function validateUser(user: User) {
358 return {
359 firstName: !validateRequired(user.firstName)
360 ? 'First Name is Required'
361 : '',
362 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
363 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
364 };
365}
366

View Extra Storybook Examples