Watch-IP

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.

Related pages