Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/

fix: detect stale deployment and prompt user to refresh (#388)

When a new version is deployed while users have the page open,
Next.js Server Actions fail with hash mismatches. This adds a
global listener that catches the specific error and shows a
persistent toast prompting the user to refresh.

Closes the issue reported by @thisismissem where editing broke
after a deployment.

authored by

Guido X Jansen and committed by
GitHub
7afc8f4b d46b971b

+37
+2
src/app/layout.tsx
··· 4 4 import { getLocale, getMessages } from 'next-intl/server'; 5 5 import { ThemeProvider } from '@/components/theme-provider'; 6 6 import { KonamiRickroll } from '@/components/konami-rickroll'; 7 + import { StaleDeploymentDetector } from '@/components/stale-deployment-detector'; 7 8 import { Toaster } from 'sonner'; 8 9 import './globals.css'; 9 10 ··· 50 51 > 51 52 {children} 52 53 <KonamiRickroll /> 54 + <StaleDeploymentDetector /> 53 55 <Toaster position="bottom-left" closeButton /> 54 56 </ThemeProvider> 55 57 </NextIntlClientProvider>
+35
src/components/stale-deployment-detector.tsx
··· 1 + 'use client'; 2 + 3 + import { useEffect, useRef } from 'react'; 4 + import { toast } from 'sonner'; 5 + 6 + const SERVER_ACTION_ERROR = 'Failed to find Server Action'; 7 + const TOAST_ID = 'stale-deployment'; 8 + 9 + export function StaleDeploymentDetector() { 10 + const shownRef = useRef(false); 11 + 12 + useEffect(() => { 13 + function handleRejection(event: PromiseRejectionEvent) { 14 + const message = event.reason?.message ?? String(event.reason ?? ''); 15 + if (!message.includes(SERVER_ACTION_ERROR)) return; 16 + if (shownRef.current) return; 17 + shownRef.current = true; 18 + 19 + toast.error('Sifa has been updated since you opened this page.', { 20 + id: TOAST_ID, 21 + duration: Infinity, 22 + description: 'Please refresh to continue editing.', 23 + action: { 24 + label: 'Refresh now', 25 + onClick: () => window.location.reload(), 26 + }, 27 + }); 28 + } 29 + 30 + window.addEventListener('unhandledrejection', handleRejection); 31 + return () => window.removeEventListener('unhandledrejection', handleRejection); 32 + }, []); 33 + 34 + return null; 35 + }