Skip to content
41 linesCodeBlameRaw
1import { useEffect } from "react";
2import { type Location, createPath, useNavigation } from "react-router";
3
4/**
5 * How long a client navigation may load before the page gives up on it and
6 * loads the address as a whole page. Longer than any read may take on the
7 * server (lib/deadline.ts `SLOW_READ_DEADLINE_MS`): a slow page that would
8 * arrive is never cut off and started again, and a stuck service shows its
9 * error page first. This catches what the server cannot: a request lost on
10 * the way, a connection that went quiet, a route's code that never arrived.
11 */
12export const STUCK_NAVIGATION_MS = 75_000;
13
14/**
15 * Where to send a navigation that has been loading too long, or null when
16 * there is nothing safe to retry. Only "loading" is retried: it reads, even
17 * after a form was posted (the action already answered and the page is
18 * reading what it changed). "submitting" is a write still on its way, and
19 * sending it again could do it twice.
20 */
21export function stuckTarget(navigation: { state: string; location?: Pick<Location, "pathname" | "search" | "hash"> | null }): string | null {
22 if (navigation.state !== "loading" || !navigation.location) return null;
23 return createPath(navigation.location);
24}
25
26/**
27 * Never leaves the page on its loading bar: a navigation still loading
28 * after `STUCK_NAVIGATION_MS` is finished as a document load of the same
29 * address, which either arrives or shows the server's error page.
30 */
31export function useStuckNavigationRescue(ms = STUCK_NAVIGATION_MS) {
32 const target = stuckTarget(useNavigation());
33 useEffect(() => {
34 if (!target) return;
35 const timer = window.setTimeout(() => {
36 console.warn(`g1t: navigation to ${target} still loading after ${ms}ms; loading it as a whole page`);
37 window.location.assign(target);
38 }, ms);
39 return () => window.clearTimeout(timer);
40 }, [target, ms]);
41}