stacker.news/components/error-boundary.js

82 lines
2.5 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'
import { decodeMinifiedStackTrace } from '@/lib/stacktrace'
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:\n ${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 {
const decodedDetails = await decodeMinifiedStackTrace(errorDetails)
await copy(decodedDetails)
toaster?.success?.('copied')
} catch (err) {
console.error(err)
toaster?.danger?.('failed to copy')
}
}
return <Button className='mt-3' onClick={onClick}>copy error information</Button>
}