MRT logoMaterial React Table

On This Page

    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

    1
    'modal' | 'cell' | 'row' | 'table' | 'custom
    'modal'
    MRT Editing Docs
    2
    boolean | (row: MRT_Row) => boolean
    MRT Editing Docs
    3
    IconButtonProps | ({ table }) => IconButtonProps
    Material UI Dialog Props
    4
    TextFieldProps | ({ cell, column, row, table }) => TextFieldProps
    Material UI TextField Props
    5
    ({ row, table }) => void
    MRT Editing Docs
    6
    OnChangeFn<MRT_Row<TData> | null>
    7
    ({ exitEditingMode, row, table, values}) => Promise<void> | void
    MRT Editing Docs
    8
    OnChangeFn<MRT_Cell<TData> | null>
    9
    ({ row, table }) => void
    MRT Editing Docs
    10
    OnChangeFn<MRT_Row<TData> | null>
    11
    ({ exitEditingMode, row, table, values}) => Promise<void> | void
    MRT Editing Docs
    12
    ({ internalEditComponents, row, table }) => ReactNode
    13
    ({ internalEditComponents, row, table }) => ReactNode

    Relevant Column Options

    1
    Array<string | { label: string; value: string }>
    2
    'text' | 'select'
    'text'
    3
    boolean | (row) => boolean
    4
    TextFieldProps | ({ cell, column, row, table }) => TextFieldProps
    Material UI TextField API

    Relevant State Options

    1
    MRT_Row
    2
    MRT_Cell
    3
    MRT_Row

    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.

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

    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', //default
    onCreatingRowSave: ({ table, values }) => {
    //validate data
    //save data to api
    table.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 api
    table.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-click
    muiTableBodyCellProps: ({ table, cell }) => ({
    onClick: () => {
    table.setEditingCell(cell);
    },
    }),
    });

    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 V2

    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.

    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 api
    table.setCreatingRow(null); //exit creating mode
    },
    onCreatingRowCancel: () => {
    //clear any validation errors
    },
    renderTopToolbarCustomActions: ({ table }) => (
    <Button
    onClick={() => {
    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

    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 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';
    30
    31const Example = () => {
    32 const [validationErrors, setValidationErrors] = useState<
    33 Record<string, string | undefined>
    34 >({});
    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: {
    48 type: 'email',
    49 required: true,
    50 error: !!validationErrors?.firstName,
    51 helperText: validationErrors?.firstName,
    52 //remove any previous validation errors when user focuses on the input
    53 onFocus: () =>
    54 setValidationErrors({
    55 ...validationErrors,
    56 firstName: undefined,
    57 }),
    58 //optionally add validation checking for onBlur or onChange
    59 },
    60 },
    61 {
    62 accessorKey: 'lastName',
    63 header: 'Last Name',
    64 muiEditTextFieldProps: {
    65 type: 'email',
    66 required: true,
    67 error: !!validationErrors?.lastName,
    68 helperText: validationErrors?.lastName,
    69 //remove any previous validation errors when user focuses on the input
    70 onFocus: () =>
    71 setValidationErrors({
    72 ...validationErrors,
    73 lastName: undefined,
    74 }),
    75 },
    76 },
    77 {
    78 accessorKey: 'email',
    79 header: 'Email',
    80 muiEditTextFieldProps: {
    81 type: 'email',
    82 required: true,
    83 error: !!validationErrors?.email,
    84 helperText: validationErrors?.email,
    85 //remove any previous validation errors when user focuses on the input
    86 onFocus: () =>
    87 setValidationErrors({
    88 ...validationErrors,
    89 email: undefined,
    90 }),
    91 },
    92 },
    93 {
    94 accessorKey: 'state',
    95 header: 'State',
    96 editVariant: 'select',
    97 editSelectOptions: usStates,
    98 muiEditTextFieldProps: {
    99 select: true,
    100 error: !!validationErrors?.state,
    101 helperText: validationErrors?.state,
    102 },
    103 },
    104 ],
    105 [validationErrors],
    106 );
    107
    108 //call CREATE hook
    109 const { mutateAsync: createUser, isPending: isCreatingUser } =
    110 useCreateUser();
    111 //call READ hook
    112 const {
    113 data: fetchedUsers = [],
    114 isError: isLoadingUsersError,
    115 isFetching: isFetchingUsers,
    116 isLoading: isLoadingUsers,
    117 } = useGetUsers();
    118 //call UPDATE hook
    119 const { mutateAsync: updateUser, isPending: isUpdatingUser } =
    120 useUpdateUser();
    121 //call DELETE hook
    122 const { mutateAsync: deleteUser, isPending: isDeletingUser } =
    123 useDeleteUser();
    124
    125 //CREATE action
    126 const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
    127 values,
    128 table,
    129 }) => {
    130 const newValidationErrors = validateUser(values);
    131 if (Object.values(newValidationErrors).some((error) => error)) {
    132 setValidationErrors(newValidationErrors);
    133 return;
    134 }
    135 setValidationErrors({});
    136 await createUser(values);
    137 table.setCreatingRow(null); //exit creating mode
    138 };
    139
    140 //UPDATE action
    141 const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({
    142 values,
    143 table,
    144 }) => {
    145 const newValidationErrors = validateUser(values);
    146 if (Object.values(newValidationErrors).some((error) => error)) {
    147 setValidationErrors(newValidationErrors);
    148 return;
    149 }
    150 setValidationErrors({});
    151 await updateUser(values);
    152 table.setEditingRow(null); //exit editing mode
    153 };
    154
    155 //DELETE action
    156 const openDeleteConfirmModal = (row: MRT_Row<User>) => {
    157 if (window.confirm('Are you sure you want to delete this user?')) {
    158 deleteUser(row.original.id);
    159 }
    160 };
    161
    162 const table = useMaterialReactTable({
    163 columns,
    164 data: fetchedUsers,
    165 createDisplayMode: 'modal', //default ('row', and 'custom' are also available)
    166 editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)
    167 enableEditing: true,
    168 getRowId: (row) => row.id,
    169 muiToolbarAlertBannerProps: isLoadingUsersError
    170 ? {
    171 color: 'error',
    172 children: 'Error loading data',
    173 }
    174 : undefined,
    175 muiTableContainerProps: {
    176 sx: {
    177 minHeight: '500px',
    178 },
    179 },
    180 onCreatingRowCancel: () => setValidationErrors({}),
    181 onCreatingRowSave: handleCreateUser,
    182 onEditingRowCancel: () => setValidationErrors({}),
    183 onEditingRowSave: handleSaveUser,
    184 //optionally customize modal content
    185 renderCreateRowDialogContent: ({ table, row, internalEditComponents }) => (
    186 <>
    187 <DialogTitle variant="h3">Create New User</DialogTitle>
    188 <DialogContent
    189 sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
    190 >
    191 {internalEditComponents} {/* or render custom edit components here */}
    192 </DialogContent>
    193 <DialogActions>
    194 <MRT_EditActionButtons variant="text" table={table} row={row} />
    195 </DialogActions>
    196 </>
    197 ),
    198 //optionally customize modal content
    199 renderEditRowDialogContent: ({ table, row, internalEditComponents }) => (
    200 <>
    201 <DialogTitle variant="h3">Edit User</DialogTitle>
    202 <DialogContent
    203 sx={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}
    204 >
    205 {internalEditComponents} {/* or render custom edit components here */}
    206 </DialogContent>
    207 <DialogActions>
    208 <MRT_EditActionButtons variant="text" table={table} row={row} />
    209 </DialogActions>
    210 </>
    211 ),
    212 renderRowActions: ({ row, table }) => (
    213 <Box sx={{ display: 'flex', gap: '1rem' }}>
    214 <Tooltip title="Edit">
    215 <IconButton onClick={() => table.setEditingRow(row)}>
    216 <EditIcon />
    217 </IconButton>
    218 </Tooltip>
    219 <Tooltip title="Delete">
    220 <IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
    221 <DeleteIcon />
    222 </IconButton>
    223 </Tooltip>
    224 </Box>
    225 ),
    226 renderTopToolbarCustomActions: ({ table }) => (
    227 <Button
    228 variant="contained"
    229 onClick={() => {
    230 table.setCreatingRow(true); //simplest way to open the create row modal with no default values
    231 //or you can pass in a row object to set default values with the `createRow` helper function
    232 // table.setCreatingRow(
    233 // createRow(table, {
    234 // //optionally pass in default values for the new row, useful for nested data or other complex scenarios
    235 // }),
    236 // );
    237 }}
    238 >
    239 Create New User
    240 </Button>
    241 ),
    242 state: {
    243 isLoading: isLoadingUsers,
    244 isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
    245 showAlertBanner: isLoadingUsersError,
    246 showProgressBars: isFetchingUsers,
    247 },
    248 });
    249
    250 return <MaterialReactTable table={table} />;
    251};
    252
    253//CREATE hook (post new user to api)
    254function useCreateUser() {
    255 const queryClient = useQueryClient();
    256 return useMutation({
    257 mutationFn: async (user: User) => {
    258 //send api update request here
    259 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
    260 return Promise.resolve();
    261 },
    262 //client side optimistic update
    263 onMutate: (newUserInfo: User) => {
    264 queryClient.setQueryData(
    265 ['users'],
    266 (prevUsers: any) =>
    267 [
    268 ...prevUsers,
    269 {
    270 ...newUserInfo,
    271 id: (Math.random() + 1).toString(36).substring(7),
    272 },
    273 ] as User[],
    274 );
    275 },
    276 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
    277 });
    278}
    279
    280//READ hook (get users from api)
    281function useGetUsers() {
    282 return useQuery<User[]>({
    283 queryKey: ['users'],
    284 queryFn: async () => {
    285 //send api request here
    286 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
    287 return Promise.resolve(fakeData);
    288 },
    289 refetchOnWindowFocus: false,
    290 });
    291}
    292
    293//UPDATE hook (put user in api)
    294function useUpdateUser() {
    295 const queryClient = useQueryClient();
    296 return useMutation({
    297 mutationFn: async (user: User) => {
    298 //send api update request here
    299 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
    300 return Promise.resolve();
    301 },
    302 //client side optimistic update
    303 onMutate: (newUserInfo: User) => {
    304 queryClient.setQueryData(
    305 ['users'],
    306 (prevUsers: any) =>
    307 prevUsers?.map((prevUser: User) =>
    308 prevUser.id === newUserInfo.id ? newUserInfo : prevUser,
    309 ),
    310 );
    311 },
    312 // onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
    313 });
    314}
    315
    316//DELETE hook (delete user in api)
    317function useDeleteUser() {
    318 const queryClient = useQueryClient();
    319 return useMutation({
    320 mutationFn: async (userId: string) => {
    321 //send api update request here
    322 await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
    323 return Promise.resolve();
    324 },
    325 //client side optimistic update
    326 onMutate: (userId: string) => {
    327 queryClient.setQueryData(
    328 ['users'],
    329 (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

    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 color
    helperText: validationErrors.age, //show error message in helper text.
    required: true,
    type: 'number',
    onChange: (event) => {
    const value = event.target.value;
    //validation logic
    if (!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._valueCache[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 button
    Cell: ({ row, table }) => (
    <Button onClick={() => table.setEditingRow(row)}>Edit Customer</Button>
    ),
    },
    },
    });