Components
UI components for Swoff — UpdatePrompt, SWProgressBar, InstallButton.
React Components
These components use the React hooks to provide ready-to-use UI elements. See Component Specs for behavioral details.
UpdatePrompt
import { useSWUpdate } from "./hooks";
export function UpdatePrompt() {
const {
updateStatus,
currentVersion,
availableVersion,
progress,
forceUpdate,
acceptUpdate,
dismissUpdate,
} = useSWUpdate();
if (updateStatus === "idle") return null;
return (
<div
className={`update-prompt ${forceUpdate ? "fullscreen" : "bottom-sheet"}`}
>
<p>
Update available: v{currentVersion} → v{availableVersion}
</p>
{updateStatus === "downloading" ? (
<div className="progress-bar" style={{ width: `${progress}%` }} />
) : (
<>
<button onClick={acceptUpdate}>Update</button>
{!forceUpdate && <button onClick={dismissUpdate}>Later</button>}
</>
)}
</div>
);
}Usage:
function App() {
return (
<>
<AppContent />
<UpdatePrompt />
</>
);
}SWProgressBar
import { useSWProgress } from "./hooks";
export function SWProgressBar() {
const { progress, status } = useSWProgress();
if (status !== "installing") return null;
return (
<div className="fixed top-0 left-0 w-full h-1 bg-gray-200">
<div
className="h-full bg-blue-500 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
);
}Usage:
function App() {
return (
<>
<SWProgressBar />
<AppContent />
</>
);
}InstallButton
import { useState, useEffect } from "react";
export function InstallButton() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
useEffect(() => {
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
setDeferredPrompt(e as any);
});
}, []);
if (!deferredPrompt) return null;
const handleInstall = async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") setDeferredPrompt(null);
};
return <button onClick={handleInstall}>Install App</button>;
}Usage:
function Header() {
return (
<header>
<h1>My App</h1>
<InstallButton />
</header>
);
}OfflineErrorBoundary
Handle errors gracefully when Service Worker or offline features fail.
import { Component, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class OfflineErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error("OfflineErrorBoundary caught:", error, errorInfo);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded">
<h2 className="text-lg font-semibold">Something went wrong</h2>
<p className="text-sm text-gray-600">
{this.state.error?.message || "Offline feature failed to load"}
</p>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => this.setState({ hasError: false, error: null })}
>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}Usage:
function App() {
return (
<OfflineErrorBoundary
fallback={
<div className="p-4">
Some offline features are unavailable. The app will work without them.
</div>
}
>
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
</OfflineErrorBoundary>
);
}With SW-specific errors:
function App() {
const [swError, setSwError] = useState(false);
useEffect(() => {
window.addEventListener("sw-error", () => setSwError(true));
}, []);
return (
<OfflineErrorBoundary
fallback={
<div className="p-4">
{swError
? "Offline caching is unavailable. Data will reload on each visit."
: "Something went wrong loading the app."}
</div>
}
>
<AppContent />
</OfflineErrorBoundary>
);
}SyncErrorBanner
Show when mutations fail to sync after multiple retries.
import { useMutationQueue } from "./hooks";
export function SyncErrorBanner() {
const { lastSync } = useMutationQueue();
if (!lastSync || lastSync.failed === 0) return null;
return (
<div className="bg-red-50 border border-red-200 p-4 rounded flex items-center justify-between">
<span>{lastSync.failed} change(s) failed to sync</span>
<button
className="text-sm text-red-600 hover:underline"
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}Next Steps
- React Hooks — underlying hooks
- Component Specs — behavioral requirements
Swoff