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.languageor the browser'sAccept-Languageheader — 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
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
}
}
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>
);
}
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 };
}
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>
);
}
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 };
}
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>
);
}
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 };
}
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;
Use case 6 — Locale detection (language + location combined)
⚠️ Important distinction: use
navigator.languagefor 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 />;
}
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;
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>
);
}
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;
}
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();
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)
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-Languageheader, or client side usingnavigator.languageand/ornavigator.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>
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.
"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
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.