diff --git a/README.md b/README.md index 332c8591..c4417bd 100644 --- a/README.md +++ b/README.md @@ -2145,6 +2145,58 @@ export async function loader({ request }: LoaderFunctionArgs) { } ``` +### Detect if Application is Stale + +> **Note** +> This depends on `react` and `@remix-run/react`. + +The `useAppIsStale` hook lets you detect if the application client is stale compared to the server version, this means the user is seeing an old version of the application and should reload the page. + +```tsx +import { useAppIsStale } from "remix-utils/use-app-is-stale"; + +export default function Component() { + let isStale = useAppIsStale(); + + // Run an effect to ask the user to reload + useEffect(() => { + if (!isStale) return; + if (window.confirm("The app is stale. Reload?")) window.location.reload(); + }, [isStale]); + + // Or render a UI to tell the user to reload. + if (isStale) { + return ( +
+

There's a new version of the app, please reload the page

+ +
+ ); + } + + return
Normal content
; +} +``` + +For this hook to work is required an API endpoint to compare the client vs the server version. + +The default endpoint is expected to work under `/api/version`, and the example content is: + +```ts +// app/routes/api.version.ts +import { json } from "@remix-run/node"; // or /cloudflare or /deno +import * as build from "@remix-run/dev/server-build"; + +export async function loader() { + return json({ version: build.assets.version }); +} +``` + +The shape must always be `{ version: string }`.\ + +> ![TIP] +> Use `useAppIsStale({ path: "/api/v1/version" })` to customize the path of the API endpoint. + ## Author - [Sergio Xalambrí](https://sergiodxa.com) diff --git a/src/react/use-app-is-stale.ts b/src/react/use-app-is-stale.ts new file mode 100644 index 0000000..51169ff --- /dev/null +++ b/src/react/use-app-is-stale.ts @@ -0,0 +1,52 @@ +import { useFetcher } from "@remix-run/react"; +import { useEffect } from "react"; +import { useHydrated } from "./use-hydrated.js"; + +interface AppIsStaleOptions { + /** + * The path of the endpoint used to check the server version. + * @default "/api/version" + */ + path?: string; + /** + * The interval in milliseconds to check the server version. + * @default 5000 + */ + intervalMs?: number; +} + +/** + * Checks if the app is stale by comparing the server version with the client + * version. + * @param options The options for the hook. + * @returns `true` if the app is stale, `false` otherwise. + * @example + * let isStale = useAppIsStale(); + * useEffect(() => { + * if (!isStale) return; + * if (window.confirm("The app is stale. Reload?")) { + * window.location.reload(); + * } + * }, [isStale]); + */ +export function useAppIsStale({ + path = "/api/version", + intervalMs = 5000, +}: AppIsStaleOptions = {}) { + let isHydrated = useHydrated(); + let { load, data } = useFetcher<{ version: string }>(); + + let isStale = false; + + if (isHydrated && data) { + isStale = data.version !== window.__remixManifest?.version; + } + + useEffect(() => { + if (isStale) return; + let id = setInterval(() => load(path), intervalMs); + return () => clearInterval(id); + }, [load, path, intervalMs, isStale]); + + return isStale; +}