Reactでコールバック形式のAPIをPromiseとuseEffectで使う方法

React

こんにちは、白々さじきです。

今回は、コールバック形式のAPIをPromiseでラップして、ReactのuseEffectで使う方法についてまとめます。

Geolocation APIで現在地を取得しようとしたとき、return しても値が取れなくてハマったので、備忘録も兼ねて書きます。(私の学習のために生成AIに作らせた文章を一部加筆修正しました。)

結論

コールバック形式のAPIは Promise でラップすると await で使えるようになります。Reactでは await はコンポーネントのトップレベルで使えないため、useEffect の中で async 関数を定義して呼びます。

コールバック形式のAPIとは

まず、今回の詰まりポイントを整理します。

Geolocation APIの getCurrentPosition はこういう形の関数です。

navigator.geolocation.getCurrentPosition(
  (position) => { /* 成功したときに呼ばれる */ },
  (error)    => { /* 失敗したときに呼ばれる */ }
);

処理が終わったあとに「コールバック関数」を呼ぶ仕組みなので、return で値を外に返せません。

function getPosition() {
  navigator.geolocation.getCurrentPosition((position) => {
    return position.coords; // ← ここで return しても外には届かない
  });
}

const pos = getPosition(); // undefined になる

最初、なぜ undefined になるのか分かりませんでした。コールバックの中の return は、外側の関数の戻り値にはならないんですよね。

Promise でラップする

コールバック形式のAPIを await で使えるようにするには、Promise でラップします。

Promise は「将来の成功または失敗を表すオブジェクト」です。resolve(値) を呼ぶと成功、reject(エラー) を呼ぶと失敗になります。

function getCurrentPosition(): Promise<{ latitude: number; longitude: number }> {
  if (!navigator.geolocation) {
    return Promise.reject(new Error('Geolocation is not supported'));
  }

  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (position) => {
        // 成功したら resolve で値を渡す
        resolve({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        });
      },
      () => {
        // 失敗したら reject でエラーを渡す
        reject(new Error('位置情報の取得に失敗しました'));
      },
    );
  });
}

これで await getCurrentPosition() と書けるようになります。

コンポーネントのトップレベルで await は使えない

awaitasync 関数の中でしか使えません。Reactのコンポーネント関数は通常の関数なので、直接 await を書くとエラーになります。

// NG: コンポーネントのトップレベルで await は使えない
const MyComponent: FC = () => {
  const result = await getCurrentPosition(); // エラー
};

useEffect の中で async 関数を定義する

Reactで非同期処理を書くときは、useEffect の中で async 関数を定義して呼ぶのが定番のパターンです。

useEffect 自体を async にするのはNGです。useEffect のコールバックはクリーンアップ関数を返す必要があり、async にすると Promise が返ってしまいます。

// NG: useEffect 自体を async にしてはいけない
useEffect(async () => {
  const result = await getCurrentPosition();
}, []);

// OK: useEffect の中で async 関数を定義して呼ぶ
useEffect(() => {
  async function fetchPosition(): Promise<void> {
    const result = await getCurrentPosition();
    setPosition({ lat: result.latitude, lng: result.longitude });
  }
  fetchPosition();
}, []);

実際のコード(現在地を取得して表示する例)

const LocationDisplay: FC = () => {
  // 初期値を null にする(まだ取得できていない状態を表現)
  const [position, setPosition] = useState<{ lat: number; lng: number } | null>(null);

  useEffect(() => {
    async function fetchPosition(): Promise<void> {
      const result = await getCurrentPosition();
      setPosition({ lat: result.latitude, lng: result.longitude });
    }
    fetchPosition();
  }, []);

  if (position === null) {
    return <p>現在地を取得中...</p>;
  }

  return (
    <p>
      緯度: {position.lat} / 経度: {position.lng}
    </p>
  );
};

null 初期値と ?? の組み合わせ

useState の初期値を { lat: 0, lng: 0 } にしてしまうと、?? でのフォールバックが効かなくなります。

// NG: 初期値がオブジェクトだと ?? が効かない
const [position, setPosition] = useState({ lat: 0, lng: 0 });
const display = position ?? fallback; // 常に position が使われる(null にならないため)

// OK: null を初期値にする
const [position, setPosition] = useState<{ lat: number; lng: number } | null>(null);
const display = position ?? fallback; // position が null のとき fallback が使われる

?? は左辺が null または undefined のときだけ右辺を使います。{ lat: 0, lng: 0 } は「値がある」と判断されるため、初期値は null にしておくのがポイントです。

まとめ

  • コールバック形式のAPIを await で使うには Promise でラップする
  • resolve(値) で成功、reject(エラー) で失敗を表現する
  • コンポーネントのトップレベルで await は使えない
  • useEffect の中で async 関数を定義して呼ぶのが定番パターン
  • 「まだ取得できていない」状態は null 初期値で表現し、?? でフォールバックする

コールバック形式のAPIをPromiseでラップするのは最初とまどいましたが、仕組みを理解してからは自然に書けるようになりました。同じところで詰まっている方の参考になれば幸いです。

参考リンク

サポートのお願い

下記リンクからお買い物いただけると、ブログ運営のための費用が増え、有料サービスを利用した記事作成が可能になります。ご協力よろしくお願いします!



コメント

タイトルとURLをコピーしました