diff --git a/react/features/base/ui/components/web/DialogErrorBoundary.tsx b/react/features/base/ui/components/web/DialogErrorBoundary.tsx new file mode 100644 index 0000000..eb7f62f --- /dev/null +++ b/react/features/base/ui/components/web/DialogErrorBoundary.tsx @@ -0,0 +1,88 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; +import { connect } from 'react-redux'; + +import { IStore } from '../../../../app/types'; +import logger from '../../../app/logger'; +import { hideDialog } from '../../../dialog/actions'; + +interface IProps { + + /** + * The child components. + */ + children: ReactNode; + + /** + * Redux dispatch function (injected by connect). + */ + dispatch: IStore['dispatch']; +} + +interface IState { + + /** + * Whether a rendering error has been caught. + */ + hasError: boolean; +} + +/** + * Error boundary that wraps DialogContainer to prevent dialog rendering + * errors from crashing the entire application. Without this, an error in + * any dialog (e.g., Settings) would unmount the Conference component, which + * calls hangup() and disconnects the user from the meeting. + */ +class DialogErrorBoundary extends Component { + + /** + * Initializes a new DialogErrorBoundary instance. + * + * @param {IProps} props - Component props. + */ + constructor(props: IProps) { + super(props); + this.state = { hasError: false }; + } + + /** + * React lifecycle method to derive state from an error. + * + * @param {Error} _error - The error that was thrown. + * @returns {IState} New state. + */ + static getDerivedStateFromError(_error: Error): IState { + return { hasError: true }; + } + + /** + * Logs the error and attempts to close the broken dialog. + * + * @param {Error} error - The error that was thrown. + * @param {ErrorInfo} info - React error info with component stack. + * @returns {void} + */ + override componentDidCatch(error: Error, info: ErrorInfo) { + logger.error('Dialog rendering error caught by boundary', error, info); + + // Close the broken dialog so user can continue using the app + this.props.dispatch(hideDialog('ErrorBoundary')); + + // Reset error state so the next dialog can render + this.setState({ hasError: false }); + } + + /** + * Renders children or nothing if there was an error. + * + * @returns {ReactNode} + */ + override render() { + if (this.state.hasError) { + return null; + } + + return this.props.children; + } +} + +export default connect()(DialogErrorBoundary);