DEV Community

Vix
Vix

Posted on

How I detect user country in React without a backend call

How I detect user location in React without a backend call

Location detection is one of those features that sounds simple until you start implementing it. Most tutorials tell you to set up a backend, call a geolocation API server-side, pass the result to the frontend. That works, but it's overkill for many use cases.

This article shows how to detect the user's location directly from React — no backend, no key required, no setup — using IPPubblico.org. I'll cover hooks, class components, TypeScript, JavaScript, and real-world use cases like regional content and automatic redirects.

Language vs Location — IP geolocation tells you where the user is connecting from, not which language they prefer. For UI language, always use navigator.language or the browser's Accept-Language header — they reflect the user's actual preference. Use IP geolocation for location-dependent content: currency, shipping forms, regional pricing, legal disclaimers.


The endpoint

IPPubblico exposes a JSON endpoint that returns country, city, ISP, timezone and more:

GET https://ippubblico.org/?api=1
Enter fullscreen mode Exit fullscreen mode

Sample response:

{
  "status": "ok",
  "ip": "203.0.113.42",
  "isp": "Example ISP",
  "timezone": "Europe/Rome",
  "geo": {
    "city": "Milan",
    "region": "Lombardy",
    "country": "Italy",
    "country_code": "IT",
    "lat": 45.4654,
    "lon": 9.1859
  }
}
Enter fullscreen mode Exit fullscreen mode

No registration. No key. Works directly from the browser thanks to CORS support.


Use case 1 — Basic country detection with hooks (JavaScript)

The simplest implementation: fetch the country on component mount and store it in state.

import { useState, useEffect } from 'react';

function App() {
  const [country, setCountry] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('https://ippubblico.org/?api=1')
      .then(res => res.json())
      .then(data => {
        setCountry(data.geo?.country_code ?? null);
        setLoading(false);
      })
      .catch(() => {
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Detecting location...</p>;

  return (
    <div>
      <p>Your country: {country ?? 'Unknown'}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use case 2 — Custom hook (JavaScript)

Reusable across the whole app. Put this in hooks/useIPInfo.js:

import { useState, useEffect } from 'react';

export function useIPInfo() {
  const [ipInfo, setIpInfo] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetch('https://ippubblico.org/?api=1')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setIpInfo(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, []);

  return { ipInfo, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Usage anywhere in the app:

import { useIPInfo } from './hooks/useIPInfo';

function Header() {
  const { ipInfo, loading } = useIPInfo();

  if (loading) return null;

  return (
    <header>
      <p>Connecting from {ipInfo?.geo?.country ?? 'Unknown'}</p>
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use case 3 — Custom hook (TypeScript)

Same hook with full typing. Put this in hooks/useIPInfo.ts:

import { useState, useEffect } from 'react';

interface GeoInfo {
  city: string | null;
  region: string | null;
  country: string | null;
  country_code: string | null;
  lat: number | null;
  lon: number | null;
}

interface IPInfo {
  status: string;
  ip: string;
  ipv4: string | null;
  ipv6: string | null;
  isp: string | null;
  timezone: string | null;
  geo: GeoInfo;
}

interface UseIPInfoResult {
  ipInfo: IPInfo | null;
  loading: boolean;
  error: string | null;
}

export function useIPInfo(): UseIPInfoResult {
  const [ipInfo, setIpInfo] = useState<IPInfo | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    fetch('https://ippubblico.org/?api=1')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<IPInfo>;
      })
      .then(data => {
        if (!cancelled) {
          setIpInfo(data);
          setLoading(false);
        }
      })
      .catch((err: Error) => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false)
        }
      });

    return () => { cancelled = true; };
  }, []);

  return { ipInfo, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Usage with full type safety:

import { useIPInfo } from './hooks/useIPInfo';

function LocationBadge() {
  const { ipInfo, loading, error } = useIPInfo();

  if (loading) return <span>...</span>;
  if (error) return null;

  return (
    <span>
      {ipInfo?.geo.country_code}{ipInfo?.geo.city}
    </span>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use case 4 — With Axios (JavaScript)

If your project already uses Axios, the hook looks almost identical:

import { useState, useEffect } from 'react';
import axios from 'axios';

export function useIPInfoAxios() {
  const [ipInfo, setIpInfo] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const source = axios.CancelToken.source();

    axios
      .get('https://ippubblico.org/?api=1', {
        cancelToken: source.token,
        timeout: 5000,
      })
      .then(res => {
        setIpInfo(res.data);
        setLoading(false);
      })
      .catch(err => {
        if (!axios.isCancel(err)) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => source.cancel();
  }, []);

  return { ipInfo, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Use case 5 — Class component (JavaScript)

For legacy codebases that haven't migrated to hooks yet:

import React, { Component } from 'react';

class CountryDetector extends Component {
  constructor(props) {
    super(props);
    this.state = {
      countryCode: null,
      loading: true,
      error: null,
    };
  }

  componentDidMount() {
    fetch('https://ippubblico.org/?api=1')
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => {
        this.setState({
          countryCode: data.geo?.country_code ?? null,
          loading: false,
        });
      })
      .catch(err => {
        this.setState({ error: err.message, loading: false });
      });
  }

  render() {
    const { countryCode, loading, error } = this.state;

    if (loading) return <p>Detecting location...</p>;
    if (error) return <p>Location unavailable</p>;

    return <p>Country: {countryCode ?? 'Unknown'}</p>;
  }
}

export default CountryDetector;
Enter fullscreen mode Exit fullscreen mode

Use case 6 — Locale detection (language + location combined)

⚠️ Important distinction: use navigator.language for UI language — it reflects what the user configured in their browser. Use IP geolocation only for location-dependent defaults like currency or regional content. The example below shows both correctly combined.

import { useState, useEffect } from 'react';
import i18n from './i18n'; // your i18n setup

const COUNTRY_CURRENCY_MAP = {
  IT: 'EUR',
  DE: 'EUR',
  FR: 'EUR',
  GB: 'GBP',
  US: 'USD',
  JP: 'JPY',
  CN: 'CNY',
  BR: 'BRL',
};

function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    // Step 1: set UI language from browser preference
    const browserLang = navigator.language?.split('-')[0] ?? 'en';
    i18n.changeLanguage(browserLang);

    // Step 2: set location-dependent defaults from IP
    fetch('https://ippubblico.org/?api=1')
      .then(res => res.json())
      .then(data => {
        const code = data.geo?.country_code;
        const currency = COUNTRY_CURRENCY_MAP[code] ?? 'USD';
        // use currency for pricing, shipping, etc. — not for language
        console.log('User currency:', currency);
      })
      .catch(() => {})
      .finally(() => setReady(true));
  }, []);

  if (!ready) return <div>Loading...</div>;

  return <MainApp />;
}
Enter fullscreen mode Exit fullscreen mode

Use case 7 — Automatic redirect by country

Redirect users to the correct regional URL based on their location:

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const COUNTRY_ROUTES = {
  IT: '/it',
  DE: '/de',
  FR: '/fr',
  ES: '/es',
};

function CountryRedirect() {
  const navigate = useNavigate();

  useEffect(() => {
    // only redirect on first visit
    const redirected = sessionStorage.getItem('country_redirected');
    if (redirected) return;

    fetch('https://ippubblico.org/?api=1')
      .then(res => res.json())
      .then(data => {
        const code = data.geo?.country_code;
        const route = COUNTRY_ROUTES[code];
        if (route) {
          sessionStorage.setItem('country_redirected', '1');
          navigate(route, { replace: true });
        }
      })
      .catch(() => {
        // no redirect on error — stay on default
      });
  }, [navigate]);

  return null;
}

export default CountryRedirect;
Enter fullscreen mode Exit fullscreen mode

Add it once at the top of your router:

function App() {
  return (
    <BrowserRouter>
      <CountryRedirect />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/it" element={<HomeIT />} />
        <Route path="/de" element={<HomeDE />} />
        <Route path="/fr" element={<HomeFR />} />
      </Routes>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

Caching the result

Location rarely changes between page loads. Cache the result in sessionStorage to avoid repeated API calls:

const CACHE_KEY = 'ippubblico_ipinfo';

async function getIPInfoCached() {
  const cached = sessionStorage.getItem(CACHE_KEY);
  if (cached) {
    try {
      return JSON.parse(cached);
    } catch (_) {}
  }

  const res = await fetch('https://ippubblico.org/?api=1');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();

  sessionStorage.setItem(CACHE_KEY, JSON.stringify(data));
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Handling rate limits

If you call the API too frequently from the same IP, you will receive a 429 Too Many Requests response with a Retry-After header. A simple retry handler:

async function fetchWithRetry(url, retries = 1) {
  const res = await fetch(url);

  if (res.status === 429 && retries > 0) {
    const retryAfter = parseInt(res.headers.get('Retry-After') ?? '20', 10);
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return fetchWithRetry(url, retries - 1);
  }

  return res;
}

// usage
const res = await fetchWithRetry('https://ippubblico.org/?api=1');
const data = await res.json();
Enter fullscreen mode Exit fullscreen mode

In practice, if you cache the result in sessionStorage you will almost never hit the limit.


Quick reference

Need Endpoint Response
IPv4 only https://ipv4.ippubblico.org/ 203.0.113.42
IPv6 only https://ipv6.ippubblico.org/ 2001:db8::1 or NONE
Both protocols https://ippubblico.org/?text=1 IPv4: x\nIPv6: x
Full geolocation https://ippubblico.org/?api=1 JSON with country, city, ISP

Full API documentation: ippubblico.org/docs.html


Conclusion

Detecting the user's location from React without a backend is straightforward with the right API. IPPubblico gives you everything from a plain IP address to full geolocation in a single call, with no key, no billing, and no setup.

The key takeaway: use navigator.language for language, use IP geolocation for location. They solve different problems and work best together.


Using a different approach for location detection in React? Share it in the comments.

Top comments (4)

Collapse
 
jonrandy profile image
Jon Randy 🎖️ • Edited

Detect country on app startup and set the default locale. Works with any i18n library:

You realise this is essentially the wrong thing to do? The user's browser already has a language preference. Just because a user is in a particular country does not mean that they prefer to see your website in the language of that country. People travel, their preferred language stays the same.

By all means, look up where they are to serve content relevant to that location... but respect their language choice.

Do this server side via the Accept-Language header, or client side using navigator.language and/or navigator.languages.

http Accept-Language has been around for 30 years, and web developers still refuse to use it. I am going to rant about a pet peeve of mine: websites that localize content based on ip-geolocation… | Kristofer Andersson

http Accept-Language has been around for 30 years, and web developers still refuse to use it. I am going to rant about a pet peeve of mine: websites that localize content based on ip-geolocation instead of user preferences. This is unfortunately a common thing, and it is getting increasingly common. One of the latest sites to engage in this bad site behavior is LinkedIn. On the first page render, the landing page is localized based on what country you are accessing it from rather than what language you have configured in your LinkedIn settings as your preferred language or from what language your browser tells the site that you are using. I find this very annoying, and I am sure I am not the only person who finds it annoying. Now, not only LinkedIn does this: countless other websites do the same thing. If you visit the website of a large global hotel chain, a rental car company, or an airline, there is a very good chance that you will land on a page localized to the local language of the country or region you are in, not the language you have configured in your browser and not the language you have specified in your user profile for that site. Do travel industry companies not know that their customers travel? The http Accept-Language header was part of the http 1.0 specification; a specification that was published in February 1996, 30 years ago. It has been part of every new http protocol version since then. Since then, for the past 30 years, every web browser has been telling websites on every page request which language(s) the user prefers. Yet, web developers seem to prefer using ip-geolocation for localization. Many websites also allow users to specify their preferred language in their user profile, yet many sites ignore that setting and render in the local language of the country you are visiting it from. Like LinkedIn has been doing for a couple of weeks. Can anyone explain why web developers do this? I have trouble understanding why anyone would do this. </endOfFridayRant>

favicon linkedin.com
Collapse
 
vix_2f14d2f56c1 profile image
Vix

You make a completely valid point, and I should have been clearer in the article.
Using IP geolocation to set the UI language is indeed the wrong approach — navigator.language and Accept-Language exist exactly for that purpose and reflect the user's actual preference, not their physical location.
The locale detection use case in the article is better suited for things that are genuinely location-dependent rather than language-dependent: defaulting to a regional price currency, pre-selecting a country in a shipping form, showing a local phone number in a contact section, or surfacing region-specific legal disclaimers. These are cases where the user's physical location matters regardless of their language preference.
I'll update that section to make the distinction explicit — detect location for location-relevant content, respect navigator.language for language. Thanks for the pushback, it makes the article more accurate.

Collapse
 
voidbrain profile image
daniele

"no backend, no API key" but then you call IPPubblico.org ??
Do you even know what an API is?
Plus, @jonrandy is right. Country location != user defailt language

Collapse
 
vix_2f14d2f56c1 profile image
Vix

Fair point on the language vs location distinction — already addressed in the reply above, and I'll update the article accordingly.
On the "no API key" comment: yes, IPPubblico is an API — the title means no key is required, not that no API is involved. The distinction matters in practice: no registration, no token to manage, no billing, no quota tied to an account. You paste the URL and it works. That's what "no API key" means in the context of developer tooling.
If the title is misleading I'm open to suggestions — "no key required" might be clearer.