diff --git a/src/core/ui/Widgets/PageErrorBoundary.test.tsx b/src/core/ui/Widgets/PageErrorBoundary.test.tsx new file mode 100644 index 000000000..e4d703dd6 --- /dev/null +++ b/src/core/ui/Widgets/PageErrorBoundary.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, test, vi } from "vitest"; +import { RouteErrorBoundary } from "./PageErrorBoundary"; + +const FailedPage = () => { + throw new Error("Request failed with status code 403"); +}; + +describe("RouteErrorBoundary", () => { + test("shows an informative message when a page fails to render", () => { + vi.spyOn(console, "error").mockImplementation(() => undefined); + + render( + + + + + + ); + + expect(screen.getByText("Unable to load this page")).toBeInTheDocument(); + expect(screen.getByText("Request failed with status code 403")).toBeInTheDocument(); + }); +}); diff --git a/src/core/ui/Widgets/PageErrorBoundary.tsx b/src/core/ui/Widgets/PageErrorBoundary.tsx new file mode 100644 index 000000000..321439c5e --- /dev/null +++ b/src/core/ui/Widgets/PageErrorBoundary.tsx @@ -0,0 +1,44 @@ +import { Alert, AlertTitle, Container } from "@mui/material"; +import React, { ErrorInfo, ReactNode } from "react"; +import { useLocation } from "react-router-dom"; + +type PageErrorBoundaryProps = { + children: ReactNode; +}; + +type PageErrorBoundaryState = { + error: Error | null; +}; + +class PageErrorBoundary extends React.Component { + state: PageErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): PageErrorBoundaryState { + return { error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("Unable to render page", error, errorInfo); + } + + render() { + if (this.state.error) { + return ( + + + Unable to load this page + {this.state.error.message} + + + ); + } + + return this.props.children; + } +} + +export const RouteErrorBoundary = ({ children }: PageErrorBoundaryProps) => { + const location = useLocation(); + + return {children}; +}; diff --git a/src/index.tsx b/src/index.tsx index bb8ee7c4f..b54429e39 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,9 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { + // Let the route error boundary replace otherwise blank pages when an + // initial request fails (for example, with a 403 response). + throwOnError: true, // Don't retry client errors (e.g. a 404 for a deleted/stale resource), they // won't succeed on a retry and only delay surfacing the result retry: (failureCount, error) => { diff --git a/src/routes.tsx b/src/routes.tsx index f960002e6..7c872483c 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -31,6 +31,7 @@ import { WeightOverview, } from "@/pages"; import { ExerciseDetailPage } from "@/pages/ExerciseDetails"; +import { RouteErrorBoundary } from "@/core/ui/Widgets/PageErrorBoundary"; import React from "react"; import { Route, Routes } from "react-router-dom"; @@ -42,7 +43,8 @@ import { Route, Routes } from "react-router-dom"; */ export const WgerRoutes = () => { return ( - + + } /> @@ -138,6 +140,7 @@ export const WgerRoutes = () => { } /> - + + ); };