From 4e1bee45035dbe75efc0c9c1190f40d5442dfd49 Mon Sep 17 00:00:00 2001 From: Iqbal Date: Fri, 30 Oct 2026 10:20:22 +0700 Subject: [PATCH] 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 --- src/components/ErrorBoundary.tsx | 13 +++++++++---- src/main.tsx | 12 ++++++++++-- src/styles/app.css | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 4b5ee4b..0b7363d 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -3,10 +3,15 @@ import { t } from '../i18n' import { Empty } from './Page' // 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. -export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> { +// page. The layout keys it by route, so moving to another page clears the error; main.tsx also puts +// 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 } + componentDidUpdate(prev: { resetKey?: string }) { + if (this.state.error && prev.resetKey !== this.props.resetKey) this.setState({ error: null }) + } + static getDerivedStateFromError(error: Error) { return { error } } @@ -19,11 +24,11 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { error: E const { error } = this.state if (!error) return this.props.children return ( -
+
{t('err.crashCopy')}
- {error.message} + {error.name}: {error.message}
diff --git a/src/main.tsx b/src/main.tsx index d8d6ae3..dae6da2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,9 +3,10 @@ import { createRoot } from 'react-dom/client' import { ClerkProvider } from '@clerk/react' import { enUS, idID } from '@clerk/localizations' 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 App from './App' +import { ErrorBoundary } from './components/ErrorBoundary' import { forgetReads } from './express/results' import { useLang } from './i18n' 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' }, } +// 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 +} + // Clerk's own sign-in and organization screens follow the app language. function Root() { const lang = useLang() @@ -47,7 +55,7 @@ function Root() { - + diff --git a/src/styles/app.css b/src/styles/app.css index fc41d0a..a42f6a6 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -209,3 +209,4 @@ textarea{resize:vertical;line-height:1.6} .compact-source-row>input:disabled{opacity:.4;cursor:not-allowed} .compact-brain .brain-selection label{font-size:13px} .compact-brain .brain-selection{font-size:12px} +.app-crash{max-width:560px;margin:12vh auto;padding-top:8px}