0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

バックエンド不要でReactからユーザーの位置を検出する

0
Posted at

バックエンド不要でReactからユーザーの位置を検出する

位置情報の検出は、実装し始めると思ったより複雑な機能です。多くのチュートリアルはバックエンドのセットアップ、サーバーサイドでのジオロケーションAPI呼び出し、フロントエンドへの結果の受け渡しを説明しています。それは機能しますが、多くのユースケースにとってはやり過ぎです。

この記事では、IPPubblico.org を使用してReactから直接ユーザーの位置を検出する方法を紹介します — バックエンドなし、APIキー不要、セットアップ不要。フック、クラスコンポーネント、TypeScript、JavaScript、そして地域コンテンツや自動リダイレクトなどの実際のユースケースを扱います。

言語と位置情報の違い — IPジオロケーションはユーザーがどこから接続しているかを教えてくれますが、どの言語を好むかは分かりません。UIの言語には常に navigator.language やブラウザの Accept-Language ヘッダーを使用してください — これらがユーザーの実際の設定を反映しています。IPジオロケーションは通貨、配送フォーム、地域価格、法的免責事項など、位置に依存するコンテンツに使用してください。


エンドポイント

IPPubblicは国、都市、ISP、タイムゾーンなどを返すJSONエンドポイントを提供します:

GET https://ippubblico.org/?api=1

レスポンス例(?lang=jaで日本語レスポンスも取得可能):

{
  "status": "ok",
  "ip": "203.0.113.42",
  "isp": "NTT Communications",
  "timezone": "Asia/Tokyo",
  "geo": {
    "city": "東京",
    "region": "東京都",
    "country": "日本",
    "country_code": "JP",
    "lat": 35.6893,
    "lon": 139.6899
  }
}

登録不要。APIキー不要。CORSサポートにより、ブラウザから直接動作します。


ユースケース1 — フックを使った基本的な国検出(JavaScript)

最もシンプルな実装:コンポーネントのマウント時に国を取得して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>位置情報を検出中...</p>;

  return (
    <div>
      <p>あなたの国: {country ?? '不明'}</p>
    </div>
  );
}

ユースケース2 — カスタムフック(JavaScript)

アプリ全体で再利用可能。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 };
}

アプリのどこでも使用可能:

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

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

  if (loading) return null;

  return (
    <header>
      <p>{ipInfo?.geo?.country ?? '不明'} から接続中</p>
    </header>
  );
}

ユースケース3 — カスタムフック(TypeScript)

完全な型付きの同じフック。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 };
}

完全な型安全性での使用:

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>
  );
}

ユースケース4 — Axiosを使用(JavaScript)

プロジェクトがすでにAxiosを使用している場合、フックはほぼ同じです:

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 };
}

ユースケース5 — クラスコンポーネント(JavaScript)

フックにまだ移行していないレガシーコードベース向け:

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>位置情報を検出中...</p>;
    if (error) return <p>位置情報が利用できません</p>;

    return <p>国: {countryCode ?? '不明'}</p>;
  }
}

export default CountryDetector;

ユースケース6 — ロケール検出(言語 + 位置情報の組み合わせ)

⚠️ 重要な区別: UIの言語には navigator.language を使用してください — ユーザーがブラウザで設定したものを反映しています。IPジオロケーションは通貨や地域コンテンツなど、位置に依存するデフォルト値にのみ使用してください。

import { useState, useEffect } from 'react';
import i18n from './i18n'; // i18nの設定

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

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

  useEffect(() => {
    // Step 1: ブラウザの設定からUI言語を設定
    const browserLang = navigator.language?.split('-')[0] ?? 'en';
    i18n.changeLanguage(browserLang);

    // Step 2: 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';
        // 言語ではなく、価格・配送などに通貨を使用
        console.log('ユーザーの通貨:', currency);
      })
      .catch(() => {})
      .finally(() => setReady(true));
  }, []);

  if (!ready) return <div>読み込み中...</div>;

  return <MainApp />;
}

ユースケース7 — 国別自動リダイレクト

位置情報に基づいてユーザーを正しい地域URLにリダイレクトします:

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

const COUNTRY_ROUTES = {
  JP: '/ja',
  CN: '/zh',
  KR: '/ko',
  DE: '/de',
  FR: '/fr',
  ES: '/es',
};

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

  useEffect(() => {
    // 初回訪問時のみリダイレクト
    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(() => {
        // エラー時はリダイレクトしない — デフォルトのままにする
      });
  }, [navigate]);

  return null;
}

export default CountryRedirect;

ルーターの先頭に一度だけ追加します:

function App() {
  return (
    <BrowserRouter>
      <CountryRedirect />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/ja" element={<HomeJA />} />
        <Route path="/zh" element={<HomeZH />} />
        <Route path="/ko" element={<HomeKO />} />
      </Routes>
    </BrowserRouter>
  );
}

結果のキャッシュ

位置情報はページロード間でめったに変わりません。sessionStorage に結果をキャッシュして、APIの重複呼び出しを避けます:

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;
}

レート制限の処理

同じIPから頻繁にAPIを呼び出すと、429 Too Many Requests レスポンスと Retry-After ヘッダーを受け取ります。シンプルなリトライハンドラー:

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') ?? '10', 10);
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return fetchWithRetry(url, retries - 1);
  }

  return res;
}

// 使用例
const res = await fetchWithRetry('https://ippubblico.org/?api=1');
const data = await res.json();

実際には、sessionStorage に結果をキャッシュすれば制限にほぼ引っかかることはありません。


クイックリファレンス

用途 エンドポイント レスポンス
IPv4のみ https://ipv4.ippubblico.org/ 203.0.113.42
IPv6のみ https://ipv6.ippubblico.org/ 2001:db8::1 または NONE
両プロトコル https://ippubblico.org/?text=1 IPv4: x\nIPv6: x
完全なジオロケーション https://ippubblico.org/?api=1 国、都市、ISP付きJSON
日本語レスポンス https://ippubblico.org/?api=1&lang=ja 都市・国名が日本語

完全なAPIドキュメント(日本語対応):ippubblico.org/docs.html
コードサンプル(ESP32、Raspberry Pi、Python、Node.js、Go):github.com/ippubblico/examples


まとめ

適切なAPIを使えば、バックエンドなしでReactからユーザーの位置を検出するのは簡単です。IPPubblicは1回の呼び出しで、プレーンなIPアドレスから完全なジオロケーションまで、APIキーなし、課金なし、セットアップなしで提供します。

重要なポイント:言語には navigator.language を、位置情報にはIPジオロケーションを使用してください。これらは異なる問題を解決し、組み合わせることで最も効果を発揮します。


Reactで別の位置情報検出アプローチを使っていますか?コメントで共有してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?