DevExtreme v25.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

React Chat - Edit and Delete Messages

The DevExtreme Chat UI component allows users to edit and delete messages as needs dictate.

Use a data source to allow users to edit and delete messages. DevExtreme Chat does not update the data source automatically. Implement a CustomStore with CRUD operations to handle updates. Once you configured these operations, enable editing.

Backend API
import React, { useState, useCallback } from 'react'; import { Chat, Editing, type ChatTypes } from 'devextreme-react/chat'; import { SelectBox } from 'devextreme-react/select-box'; import { Guid } from 'devextreme-react/common'; import { CustomStore, DataSource } from 'devextreme-react/common/data'; import { currentUser, messages as initialMessages, allowEditingLabel, allowDeletingLabel, editingOptions, } from './data.ts'; const editingStrategy = { enabled: true, disabled: false, custom: ({ component, message }) => { const { items, user } = component.option(); const userId = user.id; const lastNotDeletedMessage = items.findLast((item) => item.author?.id === userId && !item.isDeleted); return message.id === lastNotDeletedMessage?.id; }, }; const store: ChatTypes.Message[] = [...initialMessages]; const customStore = new CustomStore({ key: 'id', load: async () => store, insert: async (message) => { store.push(message); return message; }, }); const dataSource = new DataSource({ store: customStore, paginate: false, }); export default function App() { const [allowUpdating, setAllowUpdating] = useState(true); const [allowDeleting, setAllowDeleting] = useState(true); const onMessageEntered = useCallback(( { message }: ChatTypes.MessageEnteredEvent, ) => { const newMessage = { id: new Guid().toString(), ...message, }; dataSource.store().push([{ type: 'insert', key: newMessage.id, data: newMessage, }]); }, []); const onMessageDeleted = useCallback(( { message }: ChatTypes.MessageDeletedEvent, ) => { dataSource.store().push([{ type: 'update', key: message.id, data: { isDeleted: true }, }]); }, []); const onMessageUpdated = useCallback(( { message, text }: ChatTypes.MessageUpdatedEvent, ) => { dataSource.store().push([{ type: 'update', key: message.id, data: { text, isEdited: true }, }]); }, []); const handleAllowUpdatingChange = useCallback((e) => { const strategy = editingStrategy[e.value]; setAllowUpdating(() => strategy); }, []); const handleAllowDeletingChange = useCallback((e) => { const strategy = editingStrategy[e.value]; setAllowDeleting(() => strategy); }, []); return ( <React.Fragment> <div className="chat-container"> <Chat height={600} dataSource={dataSource} user={currentUser} reloadOnChange={false} onMessageEntered={onMessageEntered} onMessageDeleted={onMessageDeleted} onMessageUpdated={onMessageUpdated} > <Editing allowDeleting={allowDeleting} allowUpdating={allowUpdating} /> </Chat> </div> <div className="options"> <div className="caption">Options</div> <div className="option"> <span>Allow Editing:</span> <SelectBox items={editingOptions} valueExpr="key" displayExpr="text" inputAttr= {allowEditingLabel} defaultValue={editingOptions[0].key} onValueChanged={handleAllowUpdatingChange} /> </div> <div className="option"> <span>Allow Deleting:</span> <SelectBox items={editingOptions} valueExpr="key" displayExpr="text" inputAttr= {allowDeletingLabel} defaultValue={editingOptions[0].key} onValueChanged={handleAllowDeletingChange} /> </div> </div> </React.Fragment> ); }
import React, { useState, useCallback } from 'react'; import { Chat, Editing } from 'devextreme-react/chat'; import { SelectBox } from 'devextreme-react/select-box'; import { Guid } from 'devextreme-react/common'; import { CustomStore, DataSource } from 'devextreme-react/common/data'; import { currentUser, messages as initialMessages, allowEditingLabel, allowDeletingLabel, editingOptions, } from './data.js'; const editingStrategy = { enabled: true, disabled: false, custom: ({ component, message }) => { const { items, user } = component.option(); const userId = user.id; const lastNotDeletedMessage = items.findLast( (item) => item.author?.id === userId && !item.isDeleted, ); return message.id === lastNotDeletedMessage?.id; }, }; const store = [...initialMessages]; const customStore = new CustomStore({ key: 'id', load: async() => store, insert: async(message) => { store.push(message); return message; }, }); const dataSource = new DataSource({ store: customStore, paginate: false, }); export default function App() { const [allowUpdating, setAllowUpdating] = useState(true); const [allowDeleting, setAllowDeleting] = useState(true); const onMessageEntered = useCallback(({ message }) => { const newMessage = { id: new Guid().toString(), ...message, }; dataSource.store().push([ { type: 'insert', key: newMessage.id, data: newMessage, }, ]); }, []); const onMessageDeleted = useCallback(({ message }) => { dataSource.store().push([ { type: 'update', key: message.id, data: { isDeleted: true }, }, ]); }, []); const onMessageUpdated = useCallback(({ message, text }) => { dataSource.store().push([ { type: 'update', key: message.id, data: { text, isEdited: true }, }, ]); }, []); const handleAllowUpdatingChange = useCallback((e) => { const strategy = editingStrategy[e.value]; setAllowUpdating(() => strategy); }, []); const handleAllowDeletingChange = useCallback((e) => { const strategy = editingStrategy[e.value]; setAllowDeleting(() => strategy); }, []); return ( <React.Fragment> <div className="chat-container"> <Chat height={600} dataSource={dataSource} user={currentUser} reloadOnChange={false} onMessageEntered={onMessageEntered} onMessageDeleted={onMessageDeleted} onMessageUpdated={onMessageUpdated} > <Editing allowDeleting={allowDeleting} allowUpdating={allowUpdating} /> </Chat> </div> <div className="options"> <div className="caption">Options</div> <div className="option"> <span>Allow Editing:</span> <SelectBox items={editingOptions} valueExpr="key" displayExpr="text" inputAttr={allowEditingLabel} defaultValue={editingOptions[0].key} onValueChanged={handleAllowUpdatingChange} /> </div> <div className="option"> <span>Allow Deleting:</span> <SelectBox items={editingOptions} valueExpr="key" displayExpr="text" inputAttr={allowDeletingLabel} defaultValue={editingOptions[0].key} onValueChanged={handleAllowDeletingChange} /> </div> </div> </React.Fragment> ); }
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
import { type ChatTypes } from 'devextreme-react/chat'; import { Guid } from 'devextreme-react/common'; function getTimestamp(date, offsetMinutes = 0): number { return date.getTime() + offsetMinutes * 60000; } const date = new Date(); date.setHours(0, 0, 0, 0); export const currentUser: ChatTypes.User = { id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3', name: 'John Doe', }; export const supportAgent: ChatTypes.User = { id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3', name: 'Support Agent', avatarUrl: '../../../../images/petersmith.png', }; export const messages = [ { id: new Guid().toString(), timestamp: getTimestamp(date, -9), author: supportAgent, text: 'Hello, John!\nHow can I assist you today?', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: 'Hi, I\'m having trouble accessing my account.', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: 'It says my password is incorrect.', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, isDeleted: true, }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: supportAgent, text: 'I can help you with that. Can you please confirm your UserID for security purposes?', isEdited: true, }, ]; export const allowEditingLabel = { 'aria-label': 'Allow Editing' }; export const allowDeletingLabel = { 'aria-label': 'Allow Deleting' }; export const editingOptions = [ { text: 'Enabled', key: 'enabled' }, { text: 'Disabled', key: 'disabled' }, { text: 'Only the last message (custom)', key: 'custom' }, ];
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, defaultExtension: 'js', map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types/prop-types.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-react': 'npm:devextreme-react@25.1.3/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.3/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.19/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62/dist/dx-gantt.js', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme-react/common': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: 'json', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; System.config(window.config);
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('app'));
import { Guid } from 'devextreme-react/common'; function getTimestamp(date, offsetMinutes = 0) { return date.getTime() + offsetMinutes * 60000; } const date = new Date(); date.setHours(0, 0, 0, 0); export const currentUser = { id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3', name: 'John Doe', }; export const supportAgent = { id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3', name: 'Support Agent', avatarUrl: '../../../../images/petersmith.png', }; export const messages = [ { id: new Guid().toString(), timestamp: getTimestamp(date, -9), author: supportAgent, text: 'Hello, John!\nHow can I assist you today?', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: "Hi, I'm having trouble accessing my account.", }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: 'It says my password is incorrect.', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, isDeleted: true, }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: supportAgent, text: 'I can help you with that. Can you please confirm your UserID for security purposes?', isEdited: true, }, ]; export const allowEditingLabel = { 'aria-label': 'Allow Editing' }; export const allowDeletingLabel = { 'aria-label': 'Allow Deleting' }; export const editingOptions = [ { text: 'Enabled', key: 'enabled' }, { text: 'Disabled', key: 'disabled' }, { text: 'Only the last message (custom)', key: 'custom' }, ];
<!DOCTYPE html> <html lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.3/css/dx.light.css" /> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
#app { min-width: 720px; display: flex; gap: 20px; } .chat-container { display: flex; flex-grow: 1; align-items: center; justify-content: center; } .options { padding: 20px; display: flex; flex-direction: column; min-width: 280px; background-color: rgba(191, 191, 191, 0.15); gap: 16px; } .dx-chat { max-width: 480px; } .caption { font-size: var(--dx-font-size-sm); font-weight: 500; } .dx-avatar { border: 1px solid var(--dx-color-border); }

The editing object includes allowUpdating and allowDeleting properties. These Boolean options are initially set to false. To edit and delete messages, set these Boolean options to true or assign functions with custom logic.

Review this demo and learn how to delete/edit chat messages. First, ensure that "Options" are active in the panel next to the Chat component. Right-click (Control+Click on MacOS) or long-tap a message to open the context menu. Select "Delete" to remove the message; a marker is then displayed in place of the deleted message within the feed. Choose "Edit" to view the original message and update its content. Click "Send" to save changes; this will mark the message as edited.