Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/core/ui/Widgets/PageErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter>
<RouteErrorBoundary>
<FailedPage />
</RouteErrorBoundary>
</MemoryRouter>
);

expect(screen.getByText("Unable to load this page")).toBeInTheDocument();
expect(screen.getByText("Request failed with status code 403")).toBeInTheDocument();
});
});
44 changes: 44 additions & 0 deletions src/core/ui/Widgets/PageErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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<PageErrorBoundaryProps, PageErrorBoundaryState> {
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 (
<Container maxWidth="md" sx={{ mt: 2 }}>
<Alert severity="error">
<AlertTitle>Unable to load this page</AlertTitle>
{this.state.error.message}
</Alert>
</Container>
);
}

return this.props.children;
}
}

export const RouteErrorBoundary = ({ children }: PageErrorBoundaryProps) => {
const location = useLocation();

return <PageErrorBoundary key={location.pathname}>{children}</PageErrorBoundary>;
};
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
7 changes: 5 additions & 2 deletions src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -42,7 +43,8 @@ import { Route, Routes } from "react-router-dom";
*/
export const WgerRoutes = () => {
return (
<Routes>
<RouteErrorBoundary>
<Routes>
<Route path="/:lang">
<Route path="routine">
<Route index element={<RoutineOverview />} />
Expand Down Expand Up @@ -138,6 +140,7 @@ export const WgerRoutes = () => {
</main>
}
/>
</Routes>
</Routes>
</RouteErrorBoundary>
);
};
Loading