From 0e3120bd2c014b66df1d996cb7488344f28b6fac Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:12:14 +0300 Subject: [PATCH] Reload page when there is an update Closes #3185 --- app/components/layout/Footer.tsx | 9 ++++----- app/hooks/useReloadOnNewDeploy.ts | 33 +++++++++++++++++++++++++++++++ app/root.tsx | 5 +++++ app/utils/git-commit.ts | 8 ++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 app/hooks/useReloadOnNewDeploy.ts create mode 100644 app/utils/git-commit.ts diff --git a/app/components/layout/Footer.tsx b/app/components/layout/Footer.tsx index dca1062d8..65b621ac9 100644 --- a/app/components/layout/Footer.tsx +++ b/app/components/layout/Footer.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router"; import { Config } from "~/config"; import { useUser } from "~/features/auth/core/user"; import { usePatrons } from "~/hooks/swr"; +import { GIT_COMMIT } from "~/utils/git-commit"; import { API_PAGE, CONTRIBUTIONS_PAGE, @@ -16,8 +17,6 @@ import { WELCOME_PAGE, } from "~/utils/urls"; -declare const __GIT_COMMIT__: string; - import { Image } from "../Image"; import { DiscordIcon } from "../icons/Discord"; import { GitHubIcon } from "../icons/GitHub"; @@ -96,14 +95,14 @@ export function Footer() {

- {__GIT_COMMIT__ ? ( + {GIT_COMMIT ? ( - {t("footer.version")} {__GIT_COMMIT__.slice(0, 10)} + {t("footer.version")} {GIT_COMMIT.slice(0, 10)} ) : null} diff --git a/app/hooks/useReloadOnNewDeploy.ts b/app/hooks/useReloadOnNewDeploy.ts new file mode 100644 index 000000000..3e331afa5 --- /dev/null +++ b/app/hooks/useReloadOnNewDeploy.ts @@ -0,0 +1,33 @@ +import * as React from "react"; +import { z } from "zod"; +import * as PersistedState from "~/modules/persisted-state/persisted-state"; +import { GIT_COMMIT } from "~/utils/git-commit"; + +const reloadedForCommitPersisted = PersistedState.define({ + key: "reloadedForCommit", + storage: "session", + schema: z.string(), + default: "", +}); + +/** + * Reloads the page when the server starts reporting a different build than the + * one this client's JavaScript was loaded from. Pages that stay open for hours + * while revalidating (SendouQ looking above all) otherwise keep running old + * code against new loader data, breaking whenever a deploy changes the shape of + * that data. + */ +export function useReloadOnNewDeploy(serverCommit: string) { + React.useEffect(() => { + if (!GIT_COMMIT || !serverCommit || serverCommit === GIT_COMMIT) return; + + // the reload is expected to serve the new bundle, but should it not, the + // same build is never retried so the page can't end up in a reload loop + if (PersistedState.read(reloadedForCommitPersisted) === serverCommit) { + return; + } + + PersistedState.write(reloadedForCommitPersisted, serverCommit); + window.location.reload(); + }, [serverCommit]); +} diff --git a/app/root.tsx b/app/root.tsx index 95e25611b..4957b23cf 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -34,6 +34,7 @@ import * as NotificationRepository from "~/features/notifications/NotificationRe import { NOTIFICATIONS } from "~/features/notifications/notifications-contants"; import { resolveSidebarData } from "~/features/sidebar/core/sidebar.server"; import { useDebounce } from "~/hooks/useDebounce"; +import { useReloadOnNewDeploy } from "~/hooks/useReloadOnNewDeploy"; import lexendLatinUrl from "~/styles/fonts/lexend-latin.woff2?url"; import type { SendouRouteHandle } from "~/utils/remix.server"; import type { Route } from "./+types/root"; @@ -68,6 +69,7 @@ import { useChangeLanguage } from "./modules/i18n/useChangeLanguage"; import { isSupporter } from "./modules/permissions/utils"; import { SearchParamsProvider } from "./modules/search-params/hooks"; import { IS_E2E_TEST_RUN } from "./utils/e2e"; +import { GIT_COMMIT } from "./utils/git-commit"; import { allI18nNamespaces } from "./utils/i18n"; import { isRevalidation, metaTags, type SerializeFrom } from "./utils/remix"; import { requestContextMiddleware } from "./utils/request-context-middleware.server"; @@ -158,6 +160,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }) : undefined, sidebar: sidebarData, + buildCommit: GIT_COMMIT, }, { headers: { "Set-Cookie": await i18nCookie.serialize(locale) }, @@ -399,6 +402,8 @@ export default function App() { // Update 14.10.23: not sure if this still applies as the CatchBoundary is gone const data = useLoaderData(); + useReloadOnNewDeploy(data.buildCommit); + // Move overflow:hidden from html to body to allow position: sticky and position: fixed // elements to work properly when a React Aria Component disabled scrolling useEffect(() => { diff --git a/app/utils/git-commit.ts b/app/utils/git-commit.ts new file mode 100644 index 000000000..f274b1a8a --- /dev/null +++ b/app/utils/git-commit.ts @@ -0,0 +1,8 @@ +declare const __GIT_COMMIT__: string; + +/** + * Commit the running bundle was built from. Inlined at build time, meaning the + * client and the server report their own build's commit. Empty string outside + * of deployed builds (e.g. local development). + */ +export const GIT_COMMIT = __GIT_COMMIT__;