From 854c55cf940194b4a619cea083d67741bb1ff5b7 Mon Sep 17 00:00:00 2001 From: abu-abdullah22 Date: Fri, 24 Jul 2026 19:43:17 +0600 Subject: [PATCH] fix: add error boundaries to prevent full app crashes (#1270) --- .changeset/react-error-boundary.md | 5 ++ .../react/src/views/ChatLayout/ChatLayout.js | 27 ++++---- packages/react/src/views/EmbeddedChat.js | 62 ++++++++++--------- .../src/views/ErrorBoundary/ErrorBoundary.js | 49 +++++++++++++++ .../ErrorBoundary/ErrorBoundary.styles.js | 31 ++++++++++ .../views/ErrorBoundary/ErrorBoundary.test.js | 38 ++++++++++++ .../react/src/views/ErrorBoundary/index.js | 1 + 7 files changed, 173 insertions(+), 40 deletions(-) create mode 100644 .changeset/react-error-boundary.md create mode 100644 packages/react/src/views/ErrorBoundary/ErrorBoundary.js create mode 100644 packages/react/src/views/ErrorBoundary/ErrorBoundary.styles.js create mode 100644 packages/react/src/views/ErrorBoundary/ErrorBoundary.test.js create mode 100644 packages/react/src/views/ErrorBoundary/index.js diff --git a/.changeset/react-error-boundary.md b/.changeset/react-error-boundary.md new file mode 100644 index 0000000000..53b088dd02 --- /dev/null +++ b/.changeset/react-error-boundary.md @@ -0,0 +1,5 @@ +--- +'@embeddedchat/react': patch +--- + +Added React Error Boundaries to prevent application crashes on component errors. diff --git a/packages/react/src/views/ChatLayout/ChatLayout.js b/packages/react/src/views/ChatLayout/ChatLayout.js index d3202a4faa..920031a9fe 100644 --- a/packages/react/src/views/ChatLayout/ChatLayout.js +++ b/packages/react/src/views/ChatLayout/ChatLayout.js @@ -1,5 +1,6 @@ import React, { useEffect, useRef, useCallback } from 'react'; import { Box, useComponentOverrides } from '@embeddedchat/ui-elements'; +import { ErrorBoundary } from '../ErrorBoundary'; import styles from './ChatLayout.styles'; import { useChannelStore, @@ -143,17 +144,21 @@ const ChatLayout = () => { onDrop={(e) => handleDragDrop(e)} > - - + + + + + +
diff --git a/packages/react/src/views/EmbeddedChat.js b/packages/react/src/views/EmbeddedChat.js index 237fdb5a3f..7d90e43173 100644 --- a/packages/react/src/views/EmbeddedChat.js +++ b/packages/react/src/views/EmbeddedChat.js @@ -31,6 +31,8 @@ import { styles } from './EmbeddedChat.styles'; import GlobalStyles from './GlobalStyles'; import { overrideECProps } from '../lib/overrideECProps'; +import { ErrorBoundary } from './ErrorBoundary'; + const EmbeddedChat = (props) => { const [remoteOverrides, setRemoteOverrides] = useState({}); @@ -288,37 +290,39 @@ const EmbeddedChat = (props) => { return ( - - - - - {hideHeader ? null : ( - - )} + + + + + + {hideHeader ? null : ( + + )} - + -
- - - +
+ + + + ); }; diff --git a/packages/react/src/views/ErrorBoundary/ErrorBoundary.js b/packages/react/src/views/ErrorBoundary/ErrorBoundary.js new file mode 100644 index 0000000000..282e7ee532 --- /dev/null +++ b/packages/react/src/views/ErrorBoundary/ErrorBoundary.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Box, Button, Icon } from '@embeddedchat/ui-elements'; +import { styles } from './ErrorBoundary.styles'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + + +

Something went wrong.

+

+ The application encountered an unexpected error. +

+ + + + +
+ ); + } + + return this.props.children; + } +} + +ErrorBoundary.propTypes = { + children: PropTypes.node, +}; + +export default ErrorBoundary; diff --git a/packages/react/src/views/ErrorBoundary/ErrorBoundary.styles.js b/packages/react/src/views/ErrorBoundary/ErrorBoundary.styles.js new file mode 100644 index 0000000000..0efdc456f3 --- /dev/null +++ b/packages/react/src/views/ErrorBoundary/ErrorBoundary.styles.js @@ -0,0 +1,31 @@ +import { css } from '@emotion/react'; + +export const styles = { + errorContainer: css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + padding: 2rem; + text-align: center; + background-color: inherit; + color: inherit; + `, + icon: css` + margin-bottom: 1rem; + `, + heading: css` + margin-bottom: 0.5rem; + font-size: 1.25rem; + `, + text: css` + margin-bottom: 1.5rem; + opacity: 0.8; + `, + buttons: css` + display: flex; + gap: 1rem; + `, +}; diff --git a/packages/react/src/views/ErrorBoundary/ErrorBoundary.test.js b/packages/react/src/views/ErrorBoundary/ErrorBoundary.test.js new file mode 100644 index 0000000000..0d1906991c --- /dev/null +++ b/packages/react/src/views/ErrorBoundary/ErrorBoundary.test.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ErrorBoundary } from './index'; + +const ProblemChild = () => { + throw new Error('Test Error'); +}; + +describe('ErrorBoundary', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + test('renders children if no error', () => { + render( + +
Child Content
+
+ ); + + expect(screen.getByTestId('child')).toBeInTheDocument(); + }); + + test('renders fallback UI on error and logs error', () => { + render( + + + + ); + + expect(screen.getByText('Something went wrong.')).toBeInTheDocument(); + expect(console.error).toHaveBeenCalled(); + }); +}); diff --git a/packages/react/src/views/ErrorBoundary/index.js b/packages/react/src/views/ErrorBoundary/index.js new file mode 100644 index 0000000000..e5d6dda21a --- /dev/null +++ b/packages/react/src/views/ErrorBoundary/index.js @@ -0,0 +1 @@ +export { default as ErrorBoundary } from './ErrorBoundary';