IP Geolocation in React: A Visitor Location Component
Build a reusable useVisitorGeo hook around the Watch-IP JavaScript SDK, with the loading, error, and cancellation handling a real React component needs — including why the request can fire twice in development.
Before you start
- A Watch-IP account and a publishable visitor key with your app's origin (dev and production) on its allowed-origins list.
- React 18 or newer — the StrictMode double-invoke behavior described below is specific to development mode.
- @digitload/watch-ip-sdk installed (npm install @digitload/watch-ip-sdk).
1. Build a useVisitorGeo hook
Wrap the SDK's getGeo() call in a hook that tracks loading/result/error state and cancels the request if the component unmounts before it resolves.
TypeScript
import { useEffect, useState } from "react";
import { WatchIP, type GeoResponse } from "@digitload/watch-ip-sdk";
const client = new WatchIP("YOUR_PUBLISHABLE_KEY");
export function useVisitorGeo() {
const [geo, setGeo] = useState<GeoResponse | null>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
useEffect(() => {
const controller = new AbortController();
client
.getGeo({ signal: controller.signal })
.then((result) => {
setGeo(result);
setStatus("ready");
})
.catch((err) => {
if (err.name !== "AbortError") setStatus("error");
});
return () => controller.abort();
}, []);
return { geo, status };
}2. Why the request can fire twice in development
React's StrictMode intentionally mounts, unmounts, and remounts each component once in development to surface effects that don't clean up after themselves. That means this hook's effect runs twice, and the AbortController above matters: the first request is aborted by the cleanup function before it resolves, so only one real getGeo() call actually completes. Skipping the abort call wouldn't break correctness — GET /v1/geo is a read — but it would leave a redundant in-flight request and a state update the discarded first instance never uses.
3. Render loading, result, and fallback states
Use the hook the same way you'd use any other data hook, keeping a sensible default visible until status leaves 'loading'.
TypeScript (JSX)
function RegionBanner() {
const { geo, status } = useVisitorGeo();
if (status === "loading") return <p>Detecting your region…</p>;
if (status === "error" || !geo?.country) return <p>Showing the default region</p>;
return <p>Showing content for {geo.city ?? geo.country}</p>;
}4. Preserve an explicit visitor choice
Same rule as the plain-JavaScript guide: check for a stored preference before applying the suggested country, and only fall back to the hook's result when no preference exists yet.
TypeScript
const preferred = localStorage.getItem("preferredCountry");
const country = preferred ?? geo?.country ?? "US";Errors and edge cases
A disallowed origin fails before your code sees a normal response — add the app's dev and production URLs to the key's allowed origins first. Every field on GeoResponse except ip and isEUCountry is nullable, so status === 'ready' isn't enough on its own; also check the specific field you're about to render, as the fallback above does with geo?.country.
Next step
Using Next.js? Its server-rendering model changes where this request can safely run — see the Next.js guide for the client/server split and a secret-key server pattern for supplied IPs.