diff --git a/src/__tests__/UpdateToast.test.tsx b/src/__tests__/UpdateToast.test.tsx
new file mode 100644
index 0000000..4ebff28
--- /dev/null
+++ b/src/__tests__/UpdateToast.test.tsx
@@ -0,0 +1,63 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { render, cleanup } from '@testing-library/react'
+
+// Track the onRegistered callback so we can invoke it after render
+let capturedOnRegistered: ((reg: unknown) => void) | undefined
+
+vi.mock('virtual:pwa-register/react', () => ({
+ useRegisterSW: (opts: { onRegistered?: (reg: unknown) => void }) => {
+ capturedOnRegistered = opts.onRegistered
+ return {
+ needRefresh: [false, vi.fn()],
+ updateServiceWorker: vi.fn(),
+ }
+ },
+}))
+
+// Import after mock is set up
+import UpdateToast from '../components/UpdateToast'
+
+describe('UpdateToast', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ capturedOnRegistered = undefined
+ })
+
+ afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+ })
+
+ it('clears the update-check interval on unmount', () => {
+ const fakeRegistration = { update: vi.fn() }
+
+ const { unmount } = render()
+
+ // Simulate the service worker registration callback
+ capturedOnRegistered?.(fakeRegistration)
+
+ // Advance time to confirm the interval is active
+ vi.advanceTimersByTime(60 * 60 * 1000)
+ expect(fakeRegistration.update).toHaveBeenCalledTimes(1)
+
+ // Unmount and advance again — no additional calls should happen
+ unmount()
+ vi.advanceTimersByTime(60 * 60 * 1000)
+ expect(fakeRegistration.update).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not leak intervals across multiple mount/unmount cycles', () => {
+ const fakeRegistration = { update: vi.fn() }
+
+ // Mount and unmount 3 times
+ for (let i = 0; i < 3; i++) {
+ const { unmount } = render()
+ capturedOnRegistered?.(fakeRegistration)
+ unmount()
+ }
+
+ // After all unmounts, advancing time should trigger zero calls
+ vi.advanceTimersByTime(60 * 60 * 1000)
+ expect(fakeRegistration.update).toHaveBeenCalledTimes(0)
+ })
+})
diff --git a/src/components/UpdateToast.tsx b/src/components/UpdateToast.tsx
index 1696b9c..d943d65 100644
--- a/src/components/UpdateToast.tsx
+++ b/src/components/UpdateToast.tsx
@@ -1,3 +1,4 @@
+import { useEffect, useRef } from 'react'
import { useRegisterSW } from 'virtual:pwa-register/react'
import { XIcon } from './Icons'
@@ -32,16 +33,26 @@ export function UpdateToastView({ className = '', onReload, onDismiss }: UpdateT
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000 // 1 hour
export default function UpdateToast() {
+ const intervalRef = useRef | null>(null)
+
const {
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegistered(registration) {
if (!registration) return
- setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS)
+ intervalRef.current = setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS)
},
})
+ useEffect(() => {
+ return () => {
+ if (intervalRef.current !== null) {
+ clearInterval(intervalRef.current)
+ }
+ }
+ }, [])
+
if (!needRefresh) return null
return (