Editing Feature Guide
If your tables need full CRUD functionality, you can enable editing features in Material React Table.
There are five visually distinct editing modes to choose from, whether you want to let users edit data in a modal, inline one row at a time, one cell at a time, or just always have editing enabled for every cell, or a completely custom editing UI.
Relevant Table Options
# | Prop Name | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
|
| MRT Editing Docs | ||
2 |
| MRT Editing Docs | |||
3 |
| Material UI Dialog Props | |||
4 |
| Material UI TextField Props | |||
5 |
| MRT Editing Docs | |||
6 |
| ||||
7 |
| MRT Editing Docs | |||
8 |
| ||||
9 |
| MRT Editing Docs | |||
10 |
| ||||
11 |
| MRT Editing Docs | |||
12 |
|
| |||
13 |
| ||||
14 |
| ||||
Relevant Column Options
# | Column Option | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
| MRT Editing Docs | |||
2 |
| ||||
3 |
|
| |||
4 |
| ||||
5 |
| Material UI TextField API | |||
Relevant State Options
Enable Editing
To enable editing, you first need to set the enableEditing
table option to true
.
const table = useMaterialReactTable({columns,data,enableEditing: true,});
However, this is just the first step. You will need to hook up logic and event listeners, but it depends on which editDisplayMode
you want to use.
Disable Editing
Editing can be turned off on a per-column basis with the enableEditing
column option.
const columns = [{accessorKey: 'age',header: 'Age',enableEditing: false,},];
In the modal editing mode, a disabled text field will still be rendered for columns with editing disabled, but in the row, cell, and table editing modes, normal cell values will be rendered instead of text fields.
If you want to completely remove an editing textfield from the editing modal or render the normal cell value instead, you can use the Edit
column option to override the editing UI for that column.
const columns = [{accessorKey: 'age',header: 'Age',enableEditing: false,Edit: () => null, //don't render anything in the editing modal for this column//or Edit: ({ cell, renderedCellValue }) => <>{renderedCellValue}</>, //render the normal cell value instead of a text field},];
Edit Display Modes
There are five different editDisplayModes to choose from. The default is "modal"
, but you can also choose "row"
, "cell"
, "table"
, or "custom"
.
Modal Edit Display Mode
When using the default "modal"
editDisplayMode, the user will be presented with a modal dialog where they can edit the data for one row at a time. No data is saved until the user clicks the save button. Clicking the cancel button clears out any changes that were made on that row.
An onEditingRowSave
callback function prop must be provided where you will get access to the updated row data so that changes can be processed and saved. It is up to you how you handle the data. This function has a exitEditingMode
parameter that must be called in order to exit editing mode upon save. The reason for this is so that you can perform validation checks before letting the modal close.
By default, Material React Table will render all of the cells in the row as text fields in a vertical stack. You can customize or override this behavior with the muiEditRowDialogProps
table option, or the renderEditRowDialogContent
table option.
const table = useMaterialReactTable({columns,data,enableEditing: true,editDisplayMode: 'modal', //defaultonEditingRowSave: ({ table, values }) => {//validate data//save data to apitable.setEditingRow(null); //exit editing mode},onEditingRowCancel: () => {//clear any validation errors},muiEditRowDialogProps: {//optionally customize the dialog},renderEditRowDialogContent: ({ internalEditComponents, row, table }) => {//optionally, completely override the render of the dialog content//use `internalEditComponents` to render the generated text fields, or completely render your own form and inputs},});
Row Edit Display Mode
The "row"
editDisplayMode works just like the default "modal"
editDisplayMode, except that the editing components will render inline in the table instead of in a modal dialog. Only one row is made editable at a time.
By default, you will probably not want to save any data until the user clicks the save button, though you could wire up onChange
or onBlur
events to save data as the user inputs data.
const table = useMaterialReactTable({columns,data,enableEditing: true,editDisplayMode: 'row',onEditingRowSave: ({ table, values }) => {//validate data//save data to apitable.setEditingRow(null); //exit editing mode},onEditingRowCancel: () => {//clear any validation errors},});
Cell Edit Display Mode
The "cell"
editDisplayMode is a bit simpler visually. By default, a user can double-click a cell to activate editing mode, but only for that cell.
Then there is a bit of work for you to do to wire up either the onBlur
, onChange
, etc., events yourself in order to save the table data. This can be done in the muiEditTextFieldProps
table option or column definition option.
const columns = [{accessor: 'age',header: 'Age',muiEditTextFieldProps: ({ cell, row, table }) => ({onBlur: (event) => {//validate data//save data to api and/or rerender table// table.setEditingCell(null) is called automatically onBlur internally},}),},];const table = useMaterialReactTable({columns,data,enableEditing: true,editDisplayMode: 'cell',//optionally, use single-click to activate editing mode instead of default double-clickmuiTableBodyCellProps: ({ cell, column, table }) => ({onClick: () => {table.setEditingCell(cell); //set editing cell//optionally, focus the text fieldqueueMicrotask(() => {const textField = table.refs.editInputRefs.current?.[column.id];if (textField) {textField.focus();textField.select?.();}});},}),});
Table Edit Display Mode
The "table"
editDisplayMode is similar to the "cell"
editDisplayMode, but it simply has all of the data cells in the table become editable all at once. You will most likely wire up all of the logic the same way as the "cell"
editDisplayMode.
Custom Edit Display Mode
There is another option if you don't like any of the built-in editDisplayModes UI. If you want to completely handle your own editing UI, you can use the "custom"
editDisplayMode. This will give you access to the editingCell
, editingRow
, and creatingRow
state options, but MRT will not render any editing UI for you. This is common for rendering a form in a sidebar or similar.
Enable Creating
New in V3
Material React Table offers new functionality to make creating news rows of data easier. It works just like the editing features, but with separate state options and callbacks. A Blank row is added to the table or modal for the user to fill out and submit.
Create Display Modes
There are just three different createDisplayModes to choose from. The default is "modal"
, but you can also choose "row"
or "custom"
. They work exactly the same as their editDisplayMode counterparts.
Position Creating Row
New in v2.7
By default, the creating row will be added to the top of the table. You can change this behavior with the positionCreatingRow
table option.
const table = useMaterialReactTable({columns,data,enableEditing: true,createDisplayMode: 'modal',positionCreatingRow: 'bottom', //default is 'top'});
In advanced use cases, such as editing nested expanding sub-rows, you could even pass in a number to the positionCreatingRow
table option to specify the index of the row to insert the creating row before.
Trigger Create Mode
To trigger a new blank row to be added to the table, we just need to just populate the creatingRow
state option with a new blank row. This can be done with the table.setCreatingRow
table instance API. You can either pass in true
as an argument, or pass in row object with default values.
const table = useMaterialReactTable({columns,data,enableEditing: true,editDisplayMode: 'modal',createDisplayMode: 'modal',onCreatingRowSave: ({ table, values }) => {//validate data//save data to apitable.setCreatingRow(null); //exit creating mode},onCreatingRowCancel: () => {//clear any validation errors},renderTopToolbarCustomActions: ({ table }) => (<ButtononClick={() => {table.setCreatingRow(true); //simplest way to open the create row modal with no default values//or you can pass in a row object to set default values with the `createRow` helper function// table.setCreatingRow(// createRow(table, {// //optionally pass in default values for the new row, useful for nested data or other complex scenarios// }),// );}}>Create New User</Button>),});
CRUD Examples
Actions | Id | First Name | Last Name | Email | State |
---|---|---|---|---|---|
1import { useMemo, useState } from 'react';2import {3 MRT_EditActionButtons,4 MaterialReactTable,5 // createRow,6 type MRT_ColumnDef,7 type MRT_Row,8 type MRT_TableOptions,9 useMaterialReactTable,10} from 'material-react-table';11import {12 Box,13 Button,14 DialogActions,15 DialogContent,16 DialogTitle,17 IconButton,18 Tooltip,19} from '@mui/material';20import {21 QueryClient,22 QueryClientProvider,23 useMutation,24 useQuery,25 useQueryClient,26} from '@tanstack/react-query';27import { type User, fakeData, usStates } from './makeData';28import EditIcon from '@mui/icons-material/Edit';29import DeleteIcon from '@mui/icons-material/Delete';3031const Example = () => {32 const [validationErrors, setValidationErrors] = useState<33 Record<string, string | undefined>34 >({});3536 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: {48 required: true,49 error: !!validationErrors?.firstName,50 helperText: validationErrors?.firstName,51 //remove any previous validation errors when user focuses on the input52 onFocus: () =>53 setValidationErrors({54 ...validationErrors,55 firstName: undefined,56 }),57 //optionally add validation checking for onBlur or onChange58 },59 },60 {61 accessorKey: 'lastName',62 header: 'Last Name',63 muiEditTextFieldProps: {64 required: true,65 error: !!validationErrors?.lastName,66 helperText: validationErrors?.lastName,67 //remove any previous validation errors when user focuses on the input68 onFocus: () =>69 setValidationErrors({70 ...validationErrors,71 lastName: undefined,72 }),73 },74 },75 {76 accessorKey: 'email',77 header: 'Email',78 muiEditTextFieldProps: {79 type: 'email',80 required: true,81 error: !!validationErrors?.email,82 helperText: validationErrors?.email,83 //remove any previous validation errors when user focuses on the input84 onFocus: () =>85 setValidationErrors({86 ...validationErrors,87 email: undefined,88 }),89 },90 },91 {92 accessorKey: 'state',93 header: 'State',94 editVariant: 'select',95 editSelectOptions: usStates,96 muiEditTextFieldProps: {97 select: true,98 error: !!validationErrors?.state,99 helperText: validationErrors?.state,100 },101 },102 ],103 [validationErrors],104 );105106 //call CREATE hook107 const { mutateAsync: createUser, isPending: isCreatingUser } =108 useCreateUser();109 //call READ hook110 const {111 data: fetchedUsers = [],112 isError: isLoadingUsersError,113 isFetching: isFetchingUsers,114 isLoading: isLoadingUsers,115 } = useGetUsers();116 //call UPDATE hook117 const { mutateAsync: updateUser, isPending: isUpdatingUser } =118 useUpdateUser();119 //call DELETE hook120 const { mutateAsync: deleteUser, isPending: isDeletingUser } =121 useDeleteUser();122123 //CREATE action124 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({125 values,126 table,127 }) => {128 const newValidationErrors = validateUser(values);129 if (Object.values(newValidationErrors).some((error) => error)) {130 setValidationErrors(newValidationErrors);131 return;132 }133 setValidationErrors({});134 await createUser(values);135 table.setCreatingRow(null); //exit creating mode136 };137138 //UPDATE action139 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({140 values,141 table,142 }) => {143 const newValidationErrors = validateUser(values);144 if (Object.values(newValidationErrors).some((error) => error)) {145 setValidationErrors(newValidationErrors);146 return;147 }148 setValidationErrors({});149 await updateUser(values);150 table.setEditingRow(null); //exit editing mode151 };152153 //DELETE action154 const openDeleteConfirmModal = (row: MRT_Row<User>) => {155 if (window.confirm('Are you sure you want to delete this user?')) {156 deleteUser(row.original.id);157 }158 };159160 const table = useMaterialReactTable({161 columns,162 data: fetchedUsers,163 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)164 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)165 enableEditing: true,166 getRowId: (row) => row.id,167 muiToolbarAlertBannerProps: isLoadingUsersError168 ? {169 color: 'error',170 children: 'Error loading data',171 }172 : undefined,173 muiTableContainerProps: {174 sx: {175 minHeight: '500px',176 },177 },178 onCreatingRowCancel: () => setValidationErrors({}),179 onCreatingRowSave: handleCreateUser,180 onEditingRowCancel: () => setValidationErrors({}),181 onEditingRowSave: handleSaveUser,182 //optionally customize modal content183 renderCreateRowDialogContent: ({ table, row, internalEditComponents }) => (184 <>185 <DialogTitle variant="h3">Create New User</DialogTitle>186 <DialogContent187 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}188 >189 {internalEditComponents} {/* or render custom edit components here */}190 </DialogContent>191 <DialogActions>192 <MRT_EditActionButtons variant="text" table={table} row={row} />193 </DialogActions>194 </>195 ),196 //optionally customize modal content197 renderEditRowDialogContent: ({ table, row, internalEditComponents }) => (198 <>199 <DialogTitle variant="h3">Edit User</DialogTitle>200 <DialogContent201 sx={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}202 >203 {internalEditComponents} {/* or render custom edit components here */}204 </DialogContent>205 <DialogActions>206 <MRT_EditActionButtons variant="text" table={table} row={row} />207 </DialogActions>208 </>209 ),210 renderRowActions: ({ row, table }) => (211 <Box sx={{ display: 'flex', gap: '1rem' }}>212 <Tooltip title="Edit">213 <IconButton onClick={() => table.setEditingRow(row)}>214 <EditIcon />215 </IconButton>216 </Tooltip>217 <Tooltip title="Delete">218 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>219 <DeleteIcon />220 </IconButton>221 </Tooltip>222 </Box>223 ),224 renderTopToolbarCustomActions: ({ table }) => (225 <Button226 variant="contained"227 onClick={() => {228 table.setCreatingRow(true); //simplest way to open the create row modal with no default values229 //or you can pass in a row object to set default values with the `createRow` helper function230 // table.setCreatingRow(231 // createRow(table, {232 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios233 // }),234 // );235 }}236 >237 Create New User238 </Button>239 ),240 state: {241 isLoading: isLoadingUsers,242 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,243 showAlertBanner: isLoadingUsersError,244 showProgressBars: isFetchingUsers,245 },246 });247248 return <MaterialReactTable table={table} />;249};250251//CREATE hook (post new user to api)252function useCreateUser() {253 const queryClient = useQueryClient();254 return useMutation({255 mutationFn: async (user: User) => {256 //send api update request here257 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call258 return Promise.resolve();259 },260 //client side optimistic update261 onMutate: (newUserInfo: User) => {262 queryClient.setQueryData(263 ['users'],264 (prevUsers: any) =>265 [266 ...prevUsers,267 {268 ...newUserInfo,269 id: (Math.random() + 1).toString(36).substring(7),270 },271 ] as User[],272 );273 },274 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo275 });276}277278//READ hook (get users from api)279function useGetUsers() {280 return useQuery<User[]>({281 queryKey: ['users'],282 queryFn: async () => {283 //send api request here284 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call285 return Promise.resolve(fakeData);286 },287 refetchOnWindowFocus: false,288 });289}290291//UPDATE hook (put user in api)292function useUpdateUser() {293 const queryClient = useQueryClient();294 return useMutation({295 mutationFn: async (user: User) => {296 //send api update request here297 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call298 return Promise.resolve();299 },300 //client side optimistic update301 onMutate: (newUserInfo: User) => {302 queryClient.setQueryData(['users'], (prevUsers: any) =>303 prevUsers?.map((prevUser: User) =>304 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,305 ),306 );307 },308 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo309 });310}311312//DELETE hook (delete user in api)313function useDeleteUser() {314 const queryClient = useQueryClient();315 return useMutation({316 mutationFn: async (userId: string) => {317 //send api update request here318 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call319 return Promise.resolve();320 },321 //client side optimistic update322 onMutate: (userId: string) => {323 queryClient.setQueryData(['users'], (prevUsers: any) =>324 prevUsers?.filter((user: User) => user.id !== userId),325 );326 },327 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo328 });329}330331const queryClient = new QueryClient();332333const ExampleWithProviders = () => (334 //Put this with your other react-query providers near root of your app335 <QueryClientProvider client={queryClient}>336 <Example />337 </QueryClientProvider>338);339340export default ExampleWithProviders;341342const validateRequired = (value: string) => !!value.length;343const validateEmail = (email: string) =>344 !!email.length &&345 email346 .toLowerCase()347 .match(348 /^(([^<>()[\]\\.,;:\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,}))$/,349 );350351function validateUser(user: User) {352 return {353 firstName: !validateRequired(user.firstName)354 ? 'First Name is Required'355 : '',356 lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',357 email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',358 };359}360
Customizing Editing Components
You can pass any Material UI TextField Props with the muiEditTextFieldProps
table option.
const columns = [{accessor: 'age',header: 'Age',muiEditTextFieldProps: {required: true,type: 'number',variant: 'outlined',},},];
Add Validation to Editing Components
You can add validation to the editing components by using the muiEditTextFieldProps
events. You can write your validation logic and hook it up to the onBlur
, onChange
, etc., events, then set the error
and helperText
props accordingly.
If you are implementing validation, you may also need to use the onEditingRowCancel
table option to clear the validation error state.
const [validationErrors, setValidationErrors] = useState({});const columns = [{accessor: 'age',header: 'Age',muiEditTextFieldProps: {error: !!validationErrors.age, //highlight mui text field red error colorhelperText: validationErrors.age, //show error message in helper text.required: true,type: 'number',onChange: (event) => {const value = event.target.value;//validation logicif (!value) {setValidationErrors((prev) => ({ ...prev, age: 'Age is required' }));} else if (value < 18) {setValidationErrors({...validationErrors,age: 'Age must be 18 or older',});} else {delete validationErrors.age;setValidationErrors({ ...validationErrors });}},},},];
Use Custom Editing Components
If you need to use a much more complicated Editing component than the built-in textfield, you can specify a custom editing component with the Edit
column definition option.
const columns = [{accessorKey: 'email',header: 'Email',Edit: ({ cell, column, row, table }) => {const onBlur = (event) => {row._valuesCache[column.id] = event.target.value;if (isCreating) {setCreatingRow(row);} else if (isEditing) {setEditingRow(row);}};return <CustomInput onBlur={onBlur} />;},},];
Customize Actions/Edit Column
You can customize the actions column in a few different ways in the displayColumnDefOptions
prop's 'mrt-row-actions'
section.
const table = useMaterialReactTable({columns,data,displayColumnDefOptions: {'mrt-row-actions': {header: 'Edit', //change "Actions" to "Edit"size: 120,//use a text button instead of a icon buttonCell: ({ row, table }) => (<Button onClick={() => table.setEditingRow(row)}>Edit Customer</Button>),},},});