Show a crash outside a page instead of a white screen
Moving between pages sometimes left a white screen until a reload: an error outside the page's own error boundary (the layout, the sign-in gate) unmounted the whole app. An error boundary now wraps the whole app, shows the error's name and message, and tries again when the route changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,15 @@ import { t } from '../i18n'
|
|||||||
import { Empty } from './Page'
|
import { Empty } from './Page'
|
||||||
|
|
||||||
// Without this, one component throwing while rendering unmounts the whole app and leaves a white
|
// Without this, one component throwing while rendering unmounts the whole app and leaves a white
|
||||||
// page. The layout keys it by route, so moving to another page clears the error.
|
// page. The layout keys it by route, so moving to another page clears the error; main.tsx also puts
|
||||||
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
|
// one around the whole app (the layout included), cleared by a change of `resetKey`.
|
||||||
|
export class ErrorBoundary extends Component<{ children: ReactNode; resetKey?: string; className?: string }, { error: Error | null }> {
|
||||||
state = { error: null as Error | null }
|
state = { error: null as Error | null }
|
||||||
|
|
||||||
|
componentDidUpdate(prev: { resetKey?: string }) {
|
||||||
|
if (this.state.error && prev.resetKey !== this.props.resetKey) this.setState({ error: null })
|
||||||
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
static getDerivedStateFromError(error: Error) {
|
||||||
return { error }
|
return { error }
|
||||||
}
|
}
|
||||||
@@ -19,11 +24,11 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { error: E
|
|||||||
const { error } = this.state
|
const { error } = this.state
|
||||||
if (!error) return this.props.children
|
if (!error) return this.props.children
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className={this.props.className ?? 'card'}>
|
||||||
<Empty icon="info" title={t('err.crashTitle')}>
|
<Empty icon="info" title={t('err.crashTitle')}>
|
||||||
{t('err.crashCopy')}
|
{t('err.crashCopy')}
|
||||||
<br />
|
<br />
|
||||||
<small>{error.message}</small>
|
<small>{error.name}: {error.message}</small>
|
||||||
</Empty>
|
</Empty>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, paddingBottom: 24 }}>
|
<div style={{ display: 'flex', justifyContent: 'center', gap: 8, paddingBottom: 24 }}>
|
||||||
<button className="btn small" onClick={() => this.setState({ error: null })}>{t('common.retry')}</button>
|
<button className="btn small" onClick={() => this.setState({ error: null })}>{t('common.retry')}</button>
|
||||||
|
|||||||
12
src/main.tsx
12
src/main.tsx
@@ -3,9 +3,10 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import { ClerkProvider } from '@clerk/react'
|
import { ClerkProvider } from '@clerk/react'
|
||||||
import { enUS, idID } from '@clerk/localizations'
|
import { enUS, idID } from '@clerk/localizations'
|
||||||
import { MutationCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { MutationCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter, useLocation } from 'react-router-dom'
|
||||||
import { EngineError } from './api/client'
|
import { EngineError } from './api/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||||
import { forgetReads } from './express/results'
|
import { forgetReads } from './express/results'
|
||||||
import { useLang } from './i18n'
|
import { useLang } from './i18n'
|
||||||
import './styles/mockup.css'
|
import './styles/mockup.css'
|
||||||
@@ -40,6 +41,13 @@ const clerkAppearance = {
|
|||||||
variables: { colorPrimary: '#1570ef', colorText: '#101828', fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", borderRadius: '12px' },
|
variables: { colorPrimary: '#1570ef', colorText: '#101828', fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", borderRadius: '12px' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A crash outside a page (the layout, the sign-in gate) would otherwise unmount everything and leave
|
||||||
|
// a white page until a reload; moving to another page tries again.
|
||||||
|
function AppBoundary() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
return <ErrorBoundary resetKey={pathname} className="card app-crash"><App /></ErrorBoundary>
|
||||||
|
}
|
||||||
|
|
||||||
// Clerk's own sign-in and organization screens follow the app language.
|
// Clerk's own sign-in and organization screens follow the app language.
|
||||||
function Root() {
|
function Root() {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -47,7 +55,7 @@ function Root() {
|
|||||||
<ClerkProvider publishableKey={clerkPubKey} afterSignOutUrl="/" localization={lang === 'id' ? idID : enUS} appearance={clerkAppearance}>
|
<ClerkProvider publishableKey={clerkPubKey} afterSignOutUrl="/" localization={lang === 'id' ? idID : enUS} appearance={clerkAppearance}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<AppBoundary />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
|
|||||||
@@ -209,3 +209,4 @@ textarea{resize:vertical;line-height:1.6}
|
|||||||
.compact-source-row>input:disabled{opacity:.4;cursor:not-allowed}
|
.compact-source-row>input:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.compact-brain .brain-selection label{font-size:13px}
|
.compact-brain .brain-selection label{font-size:13px}
|
||||||
.compact-brain .brain-selection{font-size:12px}
|
.compact-brain .brain-selection{font-size:12px}
|
||||||
|
.app-crash{max-width:560px;margin:12vh auto;padding-top:8px}
|
||||||
|
|||||||
Reference in New Issue
Block a user