Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

70 linesCodeBlameRaw
1import { Component, type ErrorInfo, type ReactNode, useEffect, useRef, useState } from "react";
2import { TriangleAlert } from "lucide-react";
3import { useLocation, useRevalidator } from "react-router";
4
5import { Card } from "./ui/card";
6
7/** Says a section could not be loaded, without taking the page down. */
8export function Unavailable({ what }: { what: string }) {
9 return (
10 <Card asChild tone="plain" className="flex items-center gap-2 border-dashed px-4 py-5 text-sm text-muted">
11 <p>
12 <TriangleAlert size={14} className="text-warn" />
13 {what} could not be loaded just now. It will be back on the next refresh.
14 </p>
15 </Card>
16 );
17}
18
19/**
20 * Keeps a part of a page that fails to draw to itself: in its place it says
21 * that `what` could not be loaded, and the rest of the page stays. It tries
22 * again on the next refresh or navigation (whatever reloads the page's
23 * data). Data a part could not fetch is the loader's to say
24 * (lib/partial.server.ts `soft`); this catches what breaks while drawing.
25 */
26export function Boundary({ what, children, fallback }: { what: string; children: ReactNode; fallback?: ReactNode }) {
27 const { key } = useLocation();
28 const loads = useLoads();
29 return (
30 <Catch what={what} fallback={fallback} reset={`${key}:${loads}`}>
31 {children}
32 </Catch>
33 );
34}
35
36/** How many times the page's data has loaded again in place (a refresh, a form). */
37function useLoads(): number {
38 const { state } = useRevalidator();
39 const [loads, setLoads] = useState(0);
40 const was = useRef(state);
41 useEffect(() => {
42 if (was.current !== "idle" && state === "idle") setLoads((n) => n + 1);
43 was.current = state;
44 }, [state]);
45 return loads;
46}
47
48type CatchProps = { what: string; fallback?: ReactNode; reset: string; children: ReactNode };
49
50class Catch extends Component<CatchProps, { failed: boolean }> {
51 state = { failed: false };
52
53 static getDerivedStateFromError() {
54 return { failed: true };
55 }
56
57 componentDidCatch(error: unknown, info: ErrorInfo) {
58 console.error(`g1t: ${this.props.what} failed to draw`, error, info.componentStack);
59 }
60
61 componentDidUpdate(previous: CatchProps) {
62 // Fresh data, or another page: draw it again.
63 if (this.state.failed && previous.reset !== this.props.reset) this.setState({ failed: false });
64 }
65
66 render() {
67 if (this.state.failed) return this.props.fallback ?? <Unavailable what={this.props.what} />;
68 return this.props.children;
69 }
70}