March 31, 2026
React Geolocation and Device API Hooks
Modern web applications increasingly depend on device capabilities — knowing where a user is, whether they are online, what kind of network they are on, and what platform they are running. The browser exposes these through a collection of APIs (Geolocation, Network Information, Permissions, Navigator), but wiring them into React components correctly is harder than it looks. You need to manage watchers, handle permission states, clean up subscriptions, and deal with SSR — all while keeping your component code readable.
This article covers five hooks from ReactUse that wrap these device APIs into clean, reactive interfaces: useGeolocation, usePermission, useNetwork, useOnline, and usePlatform. For each hook, we will look at what the manual approach looks like, then see how the hook simplifies it. We will then build three practical examples that combine these hooks together.
1. Geolocation: Tracking the User’s Position
The Manual Approach
The Geolocation API is callback-based and requires careful cleanup:
import { useState, useEffect } from "react";
function useManualGeolocation() {
const [position, setPosition] = useState<{
latitude: number | null;
longitude: number | null;
}>({ latitude: null, longitude: null });
const [error, setError] = useState<GeolocationPositionError | null>(null);
useEffect(() => {
if (!navigator.geolocation) {
return;
}
const watchId = navigator.geolocation.watchPosition(
(pos) => {
setPosition({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
});
},
(err) => {
setError(err);
},
{ enableHighAccuracy: true }
);
return () => navigator.geolocation.clearWatch(watchId);
}, []);
return { position, error };
}
This handles the basics, but it does not expose accuracy, altitude, heading, or speed. It does not track the loading state. And you end up duplicating this in every component that needs location data.
The Hook Solution: useGeolocation
useGeolocation wraps the entire Geolocation API into a single reactive object:
import { useGeolocation } from "@reactuses/core";
function LocationDisplay() {
const { coordinates, error, loading } = useGeolocation();
if (loading) return <p>Acquiring location...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<p>Latitude: {coordinates?.latitude}</p>
<p>Longitude: {coordinates?.longitude}</p>
<p>Accuracy: {coordinates?.accuracy}m</p>
<p>Altitude: {coordinates?.altitude ?? "N/A"}</p>
<p>Speed: {coordinates?.speed ?? "N/A"}</p>
</div>
);
}
The hook sets up watchPosition internally, provides a loading flag while waiting for the first fix, exposes the full GeolocationCoordinates object (latitude, longitude, accuracy, altitude, heading, speed), and cleans up the watcher on unmount.
2. Permissions: Checking What the Browser Allows
The Manual Approach
The Permissions API is straightforward but async, and permissions can change at any time:
import { useState, useEffect } from "react";
function useManualPermission(name: PermissionName) {
const [state, setState] = useState<PermissionState>("prompt");
useEffect(() => {
let permissionStatus: PermissionStatus | null = null;
navigator.permissions.query({ name }).then((status) => {
permissionStatus = status;
setState(status.state);
status.addEventListener("change", () => {
setState(status.state);
});
});
return () => {
if (permissionStatus) {
permissionStatus.removeEventListener("change", () => {
setState(permissionStatus!.state);
});
}
};
}, [name]);
return state;
}
There is a subtle bug here: the cleanup function creates a new anonymous function reference, so the event listener is never actually removed. This is a common mistake.
The Hook Solution: usePermission
usePermission handles all of this correctly:
import { usePermission } from "@reactuses/core";
function CameraAccess() {
const cameraPermission = usePermission("camera");
return (
<div>
<p>Camera permission: {cameraPermission}</p>
{cameraPermission === "denied" && (
<p>Camera access has been blocked. Please enable it in browser settings.</p>
)}
{cameraPermission === "prompt" && (
<p>Click the button below to request camera access.</p>
)}
{cameraPermission === "granted" && (
<p>Camera is ready to use.</p>
)}
</div>
);
}
The hook returns a reactive PermissionState value ("granted", "denied", or "prompt") that updates automatically when the user changes the permission in browser settings.
3. Network Information: Connection Type and Quality
The Manual Approach
The Network Information API (navigator.connection) provides details like effective connection type and downlink speed, but it is not available in all browsers and requires event listeners:
import { useState, useEffect } from "react";
interface NetworkState {
online: boolean;
downlink?: number;
effectiveType?: string;
type?: string;
saveData?: boolean;
}
function useManualNetwork(): NetworkState {
const [state, setState] = useState<NetworkState>({
online: typeof navigator !== "undefined" ? navigator.onLine : true,
});
useEffect(() => {
const connection = (navigator as any).connection;
const updateState = () => {
setState({
online: navigator.onLine,
downlink: connection?.downlink,
effectiveType: connection?.effectiveType,
type: connection?.type,
saveData: connection?.saveData,
});
};
updateState();
window.addEventListener("online", updateState);
window.addEventListener("offline", updateState);
connection?.addEventListener("change", updateState);
return () => {
window.removeEventListener("online", updateState);
window.removeEventListener("offline", updateState);
connection?.removeEventListener("change", updateState);
};
}, []);
return state;
}
This is a lot of boilerplate for something that should be a simple read.
The Hook Solution: useNetwork
useNetwork provides the full network picture:
import { useNetwork } from "@reactuses/core";
function NetworkInfo() {
const network = useNetwork();
return (
<div>
<p>Online: {network.online ? "Yes" : "No"}</p>
<p>Connection type: {network.type ?? "Unknown"}</p>
<p>Effective type: {network.effectiveType ?? "Unknown"}</p>
<p>Downlink: {network.downlink ? `${network.downlink} Mbps` : "Unknown"}</p>
<p>Data saver: {network.saveData ? "On" : "Off"}</p>
</div>
);
}
The hook subscribes to both online/offline events and the Network Information API’s change event, providing a single reactive object that always reflects the current connection state.
4. Online Status: Simple Connectivity Detection
Sometimes you do not need the full network picture — you just need to know if the user is online or offline.
The Manual Approach
import { useState, useEffect } from "react";
function useManualOnline() {
const [online, setOnline] = useState(
typeof navigator !== "undefined" ? navigator.onLine : true
);
useEffect(() => {
const handleOnline = () => setOnline(true);
const handleOffline = () => setOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return online;
}
The Hook Solution: useOnline
useOnline reduces this to a single boolean:
import { useOnline } from "@reactuses/core";
function ConnectionBadge() {
const online = useOnline();
return (
<span style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "4px 12px",
borderRadius: 16,
backgroundColor: online ? "#dcfce7" : "#fee2e2",
color: online ? "#166534" : "#991b1b",
fontSize: 14,
}}>
<span style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: online ? "#22c55e" : "#ef4444",
}} />
{online ? "Online" : "Offline"}
</span>
);
}
5. Platform Detection: Knowing the Environment
The Manual Approach
Detecting the user’s platform involves parsing the user agent string, which is notoriously unreliable and verbose:
import { useState, useEffect } from "react";
function useManualPlatform() {
const [platform, setPlatform] = useState<string>("");
useEffect(() => {
// navigator.platform is deprecated but still widely used
const ua = navigator.userAgent;
if (/Win/.test(ua)) setPlatform("Windows");
else if (/Mac/.test(ua)) setPlatform("macOS");
else if (/Linux/.test(ua)) setPlatform("Linux");
else if (/Android/.test(ua)) setPlatform("Android");
else if (/iPhone|iPad/.test(ua)) setPlatform("iOS");
else setPlatform("Unknown");
}, []);
return platform;
}
The Hook Solution: usePlatform
usePlatform provides structured platform information:
import { usePlatform } from "@reactuses/core";
function PlatformBanner() {
const platform = usePlatform();
return (
<div>
<p>Platform: {platform}</p>
</div>
);
}
This is useful for showing platform-specific instructions, keyboard shortcuts (Cmd vs Ctrl), or download links.
Practical Example 1: Store Locator with Distance Calculator
Let’s build a store locator that shows the nearest store based on the user’s GPS position. This combines useGeolocation with usePermission for a smooth permission flow.
import { useGeolocation, usePermission } from "@reactuses/core";
interface Store {
name: string;
lat: number;
lng: number;
address: string;
}
const STORES: Store[] = [
{ name: "Downtown Store", lat: 40.7128, lng: -74.006, address: "123 Main St, New York" },
{ name: "Uptown Store", lat: 40.7831, lng: -73.9712, address: "456 Park Ave, New York" },
{ name: "Brooklyn Store", lat: 40.6782, lng: -73.9442, address: "789 Atlantic Ave, Brooklyn" },
];
function haversineDistance(
lat1: number, lon1: number,
lat2: number, lon2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function StoreLocator() {
const locationPermission = usePermission("geolocation");
const { coordinates, error, loading } = useGeolocation();
if (locationPermission === "denied") {
return (
<div style={{ padding: 24, backgroundColor: "#fef2f2", borderRadius: 8 }}>
<h3>Location Access Required</h3>
<p>
To find stores near you, please enable location access in your browser
settings and reload the page.
</p>
</div>
);
}
if (loading) {
return <p>Detecting your location...</p>;
}
if (error) {
return <p>Could not determine your location: {error.message}</p>;
}
const userLat = coordinates?.latitude ?? 0;
const userLng = coordinates?.longitude ?? 0;
const sortedStores = [...STORES]
.map((store) => ({
...store,
distance: haversineDistance(userLat, userLng, store.lat, store.lng),
}))
.sort((a, b) => a.distance - b.distance);
return (
<div>
<h2>Nearest Stores</h2>
<p style={{ color: "#6b7280", fontSize: 14 }}>
Your location: {userLat.toFixed(4)}, {userLng.toFixed(4)}
</p>
<ul style={{ listStyle: "none", padding: 0 }}>
{sortedStores.map((store) => (
<li
key={store.name}
style={{
padding: 16,
marginBottom: 8,
borderRadius: 8,
border: "1px solid #e5e7eb",
}}
>
<strong>{store.name}</strong>
<p style={{ margin: "4px 0", color: "#6b7280" }}>{store.address}</p>
<p style={{ margin: 0, fontWeight: 600 }}>
{store.distance.toFixed(1)} km away
</p>
</li>
))}
</ul>
</div>
);
}
The key detail here is the permission check. By reading usePermission("geolocation") we can show an informative message before the user even interacts with the geolocation prompt. If permission is "denied", we show instructions instead of a broken UI. If it is "prompt", the browser will ask when useGeolocation tries to access the position.
Practical Example 2: Offline-Aware Data Sync
This component adapts its behavior based on connectivity. When offline, it queues changes locally. When the connection returns, it syncs automatically. It uses useOnline for simple connectivity detection and useNetwork to decide sync strategy based on connection quality.
import { useState, useEffect, useCallback } from "react";
import { useOnline, useNetwork, useLocalStorage } from "@reactuses/core";
interface PendingChange {
id: string;
data: string;
timestamp: number;
}
function OfflineAwareEditor() {
const online = useOnline();
const network = useNetwork();
const [pendingChanges, setPendingChanges] = useLocalStorage<PendingChange[]>(
"pending-changes",
[]
);
const [syncStatus, setSyncStatus] = useState<"idle" | "syncing" | "error">("idle");
const [content, setContent] = useState("");
const isSlowConnection =
network.effectiveType === "slow-2g" || network.effectiveType === "2g";
const saveChange = useCallback(() => {
if (!content.trim()) return;
const change: PendingChange = {
id: crypto.randomUUID(),
data: content,
timestamp: Date.now(),
};
if (online && !isSlowConnection) {
// Fast connection: sync immediately
setSyncStatus("syncing");
fetch("/api/save", {
method: "POST",
body: JSON.stringify(change),
})
.then(() => setSyncStatus("idle"))
.catch(() => {
// Failed to sync -- queue it
setPendingChanges((prev) => [...(prev ?? []), change]);
setSyncStatus("error");
});
} else {
// Offline or slow connection: queue locally
setPendingChanges((prev) => [...(prev ?? []), change]);
}
}, [content, online, isSlowConnection, setPendingChanges]);
// Auto-sync when coming back online
useEffect(() => {
if (!online || !pendingChanges?.length) return;
if (isSlowConnection) return; // Wait for a better connection
setSyncStatus("syncing");
Promise.all(
pendingChanges.map((change) =>
fetch("/api/save", {
method: "POST",
body: JSON.stringify(change),
})
)
)
.then(() => {
setPendingChanges([]);
setSyncStatus("idle");
})
.catch(() => {
setSyncStatus("error");
});
}, [online, isSlowConnection]);
return (
<div style={{ maxWidth: 600 }}>
{/* Status bar */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px 16px",
marginBottom: 16,
borderRadius: 8,
backgroundColor: online ? "#f0fdf4" : "#fef9c3",
fontSize: 14,
}}
>
<span>{online ? "Online" : "Offline -- changes will be saved locally"}</span>
{isSlowConnection && online && (
<span style={{ color: "#92400e" }}>Slow connection detected</span>
)}
{(pendingChanges?.length ?? 0) > 0 && (
<span>
{pendingChanges!.length} pending change{pendingChanges!.length > 1 ? "s" : ""}
</span>
)}
</div>
{/* Editor */}
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type your content here..."
style={{
width: "100%",
height: 200,
padding: 12,
borderRadius: 8,
border: "1px solid #d1d5db",
resize: "vertical",
fontFamily: "inherit",
}}
/>
<button
onClick={saveChange}
disabled={syncStatus === "syncing" || !content.trim()}
style={{
marginTop: 12,
padding: "10px 20px",
borderRadius: 8,
border: "none",
backgroundColor: "#2563eb",
color: "#fff",
cursor: "pointer",
opacity: syncStatus === "syncing" || !content.trim() ? 0.5 : 1,
}}
>
{syncStatus === "syncing"
? "Syncing..."
: online
? "Save"
: "Save Locally"}
</button>
</div>
);
}
The combination of useOnline and useNetwork gives this component two levels of intelligence: it knows whether it can reach the server at all, and it knows whether the connection is fast enough to attempt a sync. On a 2G connection, it is smarter to queue changes and sync later than to attempt a slow request that might time out.
Practical Example 3: Permission-Gated Feature
This component checks whether the user has granted notification permission before showing a notification settings panel. If permission is denied, it explains how to fix it. This pattern works for any permission-gated feature — camera, microphone, geolocation, or notifications.
import { usePermission, usePlatform } from "@reactuses/core";
import { useState } from "react";
function NotificationSettings() {
const notifPermission = usePermission("notifications");
const platform = usePlatform();
const [frequency, setFrequency] = useState("daily");
const requestPermission = async () => {
if ("Notification" in window) {
await Notification.requestPermission();
}
};
// Show platform-specific instructions for enabling permissions
const getSettingsInstructions = () => {
const p = platform?.toLowerCase() ?? "";
if (p.includes("mac")) {
return "Go to System Settings > Notifications > your browser, and enable notifications.";
}
if (p.includes("win")) {
return "Go to Settings > System > Notifications, and enable notifications for your browser.";
}
return "Check your system notification settings and enable notifications for your browser.";
};
if (notifPermission === "denied") {
return (
<div style={{ padding: 24, backgroundColor: "#fef2f2", borderRadius: 8 }}>
<h3>Notifications Blocked</h3>
<p>
You have blocked notifications for this site. To enable them:
</p>
<ol>
<li>Click the lock icon in your browser's address bar</li>
<li>Find "Notifications" and change it to "Allow"</li>
<li>Reload the page</li>
</ol>
<p style={{ color: "#6b7280", fontSize: 14 }}>
{getSettingsInstructions()}
</p>
</div>
);
}
if (notifPermission === "prompt") {
return (
<div style={{ padding: 24, backgroundColor: "#eff6ff", borderRadius: 8 }}>
<h3>Enable Notifications</h3>
<p>Get notified about important updates and reminders.</p>
<button
onClick={requestPermission}
style={{
padding: "10px 20px",
borderRadius: 8,
border: "none",
backgroundColor: "#2563eb",
color: "#fff",
cursor: "pointer",
}}
>
Enable Notifications
</button>
</div>
);
}
// Permission granted -- show the full settings panel
return (
<div style={{ padding: 24, backgroundColor: "#f0fdf4", borderRadius: 8 }}>
<h3>Notification Settings</h3>
<p style={{ color: "#166534" }}>Notifications are enabled.</p>
<label style={{ display: "block", marginTop: 16 }}>
<strong>Frequency:</strong>
<select
value={frequency}
onChange={(e) => setFrequency(e.target.value)}
style={{ marginLeft: 8, padding: 4 }}
>
<option value="realtime">Real-time</option>
<option value="daily">Daily digest</option>
<option value="weekly">Weekly summary</option>
</select>
</label>
</div>
);
}
Notice how usePlatform is used to show platform-specific instructions when notifications are blocked. This kind of contextual help dramatically reduces the number of users who give up when they see a “permission denied” state.
When to Use useOnline vs useNetwork
Both hooks deal with connectivity, but they serve different purposes:
| Feature | useOnline | useNetwork |
|---|---|---|
| Return value | boolean | Object with online, type, effectiveType, downlink, saveData |
| Use case | Simple on/off toggle | Adaptive behavior based on connection quality |
| Browser support | All browsers | Chromium-based browsers (Network Information API) |
| Overhead | Minimal | Minimal |
Use useOnline when you only need to know if the user is connected. Use useNetwork when you need to adapt behavior based on connection quality — for example, loading lower-resolution images on slow connections, or deferring non-critical network requests.
Error Handling and SSR
All five hooks are SSR-safe. They return sensible defaults on the server and only activate their browser API subscriptions after the component mounts on the client:
useGeolocationreturnsloading: trueuntil the first position is receivedusePermissionreturns"prompt"as the default stateuseNetworkreturns{ online: true }on the serveruseOnlinereturnstrueon the serverusePlatformreturns an empty string on the server
This means you can use them in Next.js, Remix, or any SSR framework without conditional imports or dynamic loading.
Installation
npm install @reactuses/core
Then import the hooks you need:
import {
useGeolocation,
usePermission,
useNetwork,
useOnline,
usePlatform,
} from "@reactuses/core";
Related Hooks
useEventListener— subscribe to any DOM event with automatic cleanupuseSupported— check if a browser API is supported before using ituseLocalStorage— persist state to localStorage with SSR safety
ReactUse provides 100+ hooks for React. Explore them all →