stacker.news/components/error-boundary.js
SatsAllDay 64a16373c5
Fix client-side error boundary (#623)
* * add error boundary immediately above page component

* send error logs to server via LoggerContext

* display error stack and component stack in copyable text area

* wrap every context provider in an error boundary

* memoize context values to fix error boundary propagation issue

* Wrap page component in an error boundary so we can use our context utilities
for a better experience, like toast and logger

Still have a top-level error boundary that catches things from the context providers,
just in case.

Don't display the whole error stack, just because it's ugly, but make it copyable
2023-11-19 14:24:56 -06:00

81 lines
2.4 KiB
JavaScript

import { Component } from 'react'
import { StaticLayout } from './layout'
import styles from '../styles/error.module.css'
import Image from 'react-bootstrap/Image'
import copy from 'clipboard-copy'
import { LoggerContext } from './logger'
import Button from 'react-bootstrap/Button'
import { useToast } from './toast'
class ErrorBoundary extends Component {
constructor (props) {
super(props)
// Define a state variable to track whether is an error or not
this.state = {
hasError: false,
error: undefined,
errorInfo: undefined
}
}
static getDerivedStateFromError (error) {
// Update state so the next render will show the fallback UI
return { hasError: true, error }
}
getErrorDetails () {
let details = this.state.error.stack
if (this.state.errorInfo?.componentStack) {
details += `\n\nComponent stack:${this.state.errorInfo.componentStack}`
}
return details
}
componentDidCatch (error, errorInfo) {
// You can use your own error logging service here
console.log({ error, errorInfo })
this.setState({ errorInfo })
const logger = this.context
logger?.error(this.getErrorDetails())
}
render () {
// Check if the error is thrown
if (this.state.hasError) {
// You can render any custom fallback UI
const errorDetails = this.getErrorDetails()
return (
<StaticLayout footer={false}>
<Image width='500' height='375' className='rounded-1 shadow-sm' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/floating.gif`} fluid />
<h1 className={styles.status} style={{ fontSize: '48px' }}>something went wrong</h1>
{this.state.error && <CopyErrorButton errorDetails={errorDetails} />}
</StaticLayout>
)
}
// Return children components in case of no error
return this.props.children
}
}
ErrorBoundary.contextType = LoggerContext
export default ErrorBoundary
// This button is a functional component so we can use `useToast` hook, which
// can't be easily done in a class component that already consumes a context
const CopyErrorButton = ({ errorDetails }) => {
const toaster = useToast()
const onClick = async () => {
try {
await copy(errorDetails)
toaster?.success?.('copied')
} catch (err) {
console.error(err)
toaster?.danger?.('failed to copy')
}
}
return <Button className='mt-3' onClick={onClick}>copy error information</Button>
}