Modal + banner UI

React components shipping in scaffold.

7 minmedium

The React side: a banner that appears when an update is available, a modal that shows progress + the "Install" button. The scaffold ships these β€” this lesson explains the components so you can customise them.

The hook that polls

src/hooks/use-update-checker.ts
import { useQuery } from '@tanstack/react-query'
import { CheckForUpdates } from '../../wailsjs/go/main/Updater'
export function useUpdateChecker() {
const q = useQuery({
queryKey: ['updater', 'latest'],
queryFn: () => CheckForUpdates(),
refetchInterval: 6 * 60 * 60 * 1000, // 6h
refetchOnWindowFocus: false,
})
const isUpdateAvailable = !!q.data?.is_newer
return { update: q.data, isUpdateAvailable, checkNow: () => q.refetch() }
}

Cheap call β€” one HTTP round-trip to GitHub. Defaulting to 6 hours means each running app costs ~4 GitHub requests/day.

The banner

src/components/update-banner.tsx
export function UpdateBanner() {
const { update, isUpdateAvailable } = useUpdateChecker()
const [dismissed, setDismissed] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
if (!isUpdateAvailable || dismissed) return null
return (
<>
<div className="flex items-center gap-3 px-4 py-2 border-b bg-primary/5">
<Download className="h-3.5 w-3.5 text-primary" />
<span className="flex-1 text-sm">
Update available: <strong>v{update.version}</strong>
</span>
<button onClick={() => setModalOpen(true)} className="text-xs text-primary">
Update now
</button>
<button onClick={() => setDismissed(true)} className="text-muted-foreground">
<X className="h-3.5 w-3.5" />
</button>
</div>
{modalOpen && <UpdateModal update={update} onClose={() => setModalOpen(false)} />}
</>
)
}

Mount once in the root layout. Dismissal is per-session β€” the banner reappears on next launch if the update is still pending.

The modal β€” download progress + install

src/components/update-modal.tsx (key parts)
export function UpdateModal({ update, onClose }) {
const [progress, setProgress] = useState({ status: 'idle', bytesDownloaded: 0, bytesTotal: 0 })
// Auto-start the download on mount
useEffect(() => {
StartDownload(update.download_url, update.version)
}, [])
// Poll progress every 500ms
useEffect(() => {
const id = setInterval(async () => {
const p = await GetProgress()
setProgress(p)
}, 500)
return () => clearInterval(id)
}, [])
const pct = (progress.bytesDownloaded / progress.bytesTotal) * 100
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<h2>Update available</h2>
<p className="text-sm text-muted-foreground">v{APP_VERSION} β†’ v{update.version}</p>
{/* Release notes β€” capped so the install button stays visible */}
<div className="max-h-[180px] overflow-y-auto text-sm">
{update.release_notes}
</div>
{progress.status === 'downloading' && (
<>
<div className="flex justify-between text-xs">
<span>Downloading…</span>
<span>{Math.round(pct)}%</span>
</div>
<div className="h-2 bg-muted rounded">
<div className="h-full bg-primary rounded" style={{ width: pct + '%' }} />
</div>
</>
)}
{progress.status === 'downloaded' && (
<Button onClick={InstallAndRestart}>Install & restart</Button>
)}
</DialogContent>
</Dialog>
)
}
Cap the release-notes height. Long changelogs push the install button below the fold. The scaffold caps at 180px max-height + scroll. Users always see the action.

Error UX

Download fails (network blip, GitHub down). The user shouldn't be stuck. The scaffold handles four states:

  • idle β€” nothing happening
  • downloading β€” progress bar
  • downloaded β€” "Install & restart" button
  • error β€” error message + "Try again" button

Where to mount the banner

Top of App.tsx, above the routes. Always visible when an update is pending β€” but stays out of the way (one row).

<div className="h-screen flex flex-col">
<TitleBar />
<UpdateBanner /> {/* ← here */}
<main className="flex-1 overflow-auto">
{/* routes */}
</main>
</div>

Quick check

A user gets the update banner. They click Update now. The download fails halfway with a network error. What should the modal show?

Try it

Inspect the three scaffolded components:

  1. src/components/update-banner.tsx
  2. src/components/update-modal.tsx
  3. src/hooks/use-update-checker.ts

In notes.md, write down:

  • The polling interval the hook uses
  • The four progress states the modal handles
  • Where the dismissal state is stored (component / global)

What's next

Last lesson of the chapter β€” the release script that builds, tags, and publishes a new version to GitHub so the updater can find it.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled β€” suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub