【ハンズオン】ReactとOpenWeatherMap APIで天気アプリを作ろう【TypeScript】

## はじめに
OpenWeatherMap APIは、世界中の天気情報を提供するオンラインサービスで、現在気象や天気予報を含む様々な気象データを無料で利用できるAPIを提供しています。

Webやモバイルアプリケーションの開発者向けに、JSON、XML、HTML形式で天気情報を配信しています。

今回、ReactとOpenWeatherMap APIを使って、都市を入力したらその都市の天気が表示されるような簡単な天気アプリを作ってみました。

## 1. 環境構築

Node環境があることを前提にして行います。

templateはtypescriptを指定して、Google Cloud Vision APIをコールするためにaxiosをインストールしておきます。

npm startしてReactの初期画面が表示されれば成功です。

```
$ npx create-react-app city-weather-app --template typescript
$ cd city-weather-app
$ npm i axios
$ npm start
```

## 2. OpenWeatherMap APIの設定

OpenWeatherMap APIにアクセスして、ユーザー登録をします。

https://openweathermap.org/

![スクリーンショット 2024-05-28 18.45.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/e06f98e6-ea92-1814-7f62-898e607d5933.png)

ユーザー名、メールアドレス、パスワードを入力して、「Create Account」を押します。

![スクリーンショット 2024-05-28 18.45.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/c468383b-2e6f-9b85-6677-acaa00713aeb.png)

アカウント登録が完了したら、「My API keys」からAPI Keyをコピーしておきます。

## 3. 環境変数の設定

作成したAPIキーを.envファイルを作成し、環境変数を設定します。

creare-react-appで作ったプロジェクトはprocess.env.REACT_APP_から始まる変数を読み込みができるようになっています。

先ほどコピーしたAPI Keyを設定します。

```.env
REACT_APP_OPENWEATHERMAP_API_KEY=xxxxxxxxxxxxx
```

## 4. OpenWeatherMap APIを呼び出す

API Keyを設定できたら、実際にOpenWeatherMap APIを呼び出してみます。

こちらの公式ドキュメントを参考にAPIの呼び出しを行います。

https://openweathermap.org/current#:~:text=for%20this%20functionality.-,Built%2Din%20API%20request%20by%20city%20name,-You%20can%20call

今回は入力した都市の天気情報を表示したいので、こちらのAPIの呼び方を参考にします。
```
https://api.openweathermap.org/data/2.5/weather?q={city name}&appid={API key}
```

では実際にAPIを呼び出してみます。まず天気アプリ画面用のコンポーネントを作成します。

先ほどのAPIの呼び出しを参考にコードを書いていきます。

```Weather.tsx
import axios from "axios";

const Weather = () => {
  const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
  const city = 'Tokyo'

  const apiCall = async () => {
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
  
      console.log('===================== response data =====================');
      console.log(response.data);
      
    } catch (error) {
      console.error('Error has occured:', error);
    }
  }

  return (
    <div>
      <button onClick={apiCall}>API Call</button>
    </div>
  )
}

export default Weather;
```

API呼び出しの全体的なコードは上記のようになります。

```ts
const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
const city = 'Tokyo'

...

const response = await axios.get(
  `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
);
```

まずAPIを呼び出しているところから見ていきます。

APIの呼び出しでcityとapiKeyの2つが変数になっています。そのため、apiKeyは先ほど定義した環境変数から取得し、cityは一旦固定で「Tokyo」としておきます。

これで、APIを呼び出すことができます。


```ts
console.log('===================== response data =====================');
console.log(response.data);
```

次に呼び出したAPIのレスポンスをコンソール上に表示させます。
console.logで結果を出力することで、ブラウザのコンソール上で実際にどんなレスポンスデータが返ってきているか確認することができます。


```ts
return (
  <div>
    <button onClick={apiCall}>API Call</button>
  </div>
)
```
トリガーはこのbuttonにしています。ボタンを押すとAPIが呼び出されるようにしています。

```App.tsx
import Weather from './Weather';

function App() {
  return (
    <div className="App">
      <Weather />
    </div>
  );
};

export default App;
```

最後にApp.tsxにWeatherコンポーネントを呼び出して、完了です。

![スクリーンショット 2024-05-29 13.45.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/2cbc8dd3-a23f-fd24-a6f5-d0501d0ac941.png)

npm startして実際にローカルの画面を表示すると、「API Call」のボタンが表示され、ボタンを押すと、右側のコンソールのようにAPIのレスポンスデータが表示されます。

## 5. OpenWeatherMap APIのレスポンスデータを画面に表示する

次にAPIのレスポンスデータを画面に表示させていきます。

```Weather.tsx
import axios from "axios";
import { useState } from "react";

const Weather = () => {
  const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
  const city = 'Tokyo'

  const [weather, setWeather] = useState<any>(null);

  const apiCall = async () => {
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
  
      setWeather(response.data);
    } catch (error) {
      console.error('Error has occured:', error);
    }
  }

  return (
    <div>
      <button onClick={apiCall}>API Call</button>
      {weather && (
        <div>
          <h2>{weather.name}</h2>
          <p>Temperature: {weather.main.temp}°C</p>
          <p>Weather: {weather.weather[0].description}</p>
        </div>
      )}
    </div>
  )
}

export default Weather;
```

全体像としては上記のとおりです。

```ts
const [weather, setWeather] = useState<any>(null);

const apiCall = async () => {
  try {
    const response = await axios.get(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
    );
  
    setWeather(response.data);
  } catch (error) {
    console.error('Error has occured:', error);
  }
}
```

useState フックを使って、2つの要素を持つ状態変数を宣言しています。

weatherは天気情報を入れるための変数で、初期値はnullです。setWeatherはweatherの値を更新するための関数です。


```ts
return (
  <div>
    <button onClick={apiCall}>API Call</button>
    {weather && (
      <div>
        <h2>{weather.name}</h2>
        <p>Temperature: {weather.main.temp}°C</p>
        <p>Weather: {weather.weather[0].description}</p>
      </div>
    )}
  </div>
)
```

{weather && ( ... ) } 部分は、weather変数に値が存在する場合のみ、divタグ内の内容を表示します。

weatherがnullまたはundefinedの場合は、div タグ内の内容は表示されません。

weatherに値がある場合、都市名・気温・天気を表示するようにします。

![スクリーンショット 2024-05-29 17.28.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/bd9b2e51-bb9a-1e01-afd9-aac132a2618d.png)

実際に実行してみると上記のように表示されることが確認できます。


## 6. 都市を入力して表示できるようにする

現状、東京に固定して天気情報を取得しているため、入力値に合わせて天気情報を取得するようにします。

```Weather.tsx
import axios from "axios";
import { useState } from "react";

const Weather = () => {
  const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
  const [weather, setWeather] = useState<any>(null);
  // 追加
  const [city, setCity] = useState('');

  // 追加
  const inputCity = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCity(event.target.value);
  };

  const apiCall = async () => {
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
  
      setWeather(response.data);
      
    } catch (error) {
      console.error('Error has occured:', error);
    }
  }

  return (
    <div>
      // 追加
      <input
        type="text"
        value={city}
        onChange={inputCity}
        placeholder="都市を入力してください"
      />
      <button onClick={apiCall}>API Call</button>
      {weather && (
        <div>
          <h2>{weather.name}</h2>
          <p>Temperature: {weather.main.temp}°C</p>
          <p>Weather: {weather.weather[0].description}</p>
        </div>
      )}
    </div>
  )
}

export default Weather;
```

追加したコードが入力した都市の天気情報を取得して、表示するようにして箇所です。

```ts
const [city, setCity] = useState('');

const inputCity = (event: React.ChangeEvent<HTMLInputElement>) => {
  setCity(event.target.value);
};

...

<input
  type="text"
  value={city}
  onChange={inputCity}
  placeholder="都市を入力してください"
/>

```

onChangeはフォーム要素の値が変更されたときに発生するイベントハンドラで、inputタグを追加し、onChangeイベントでcity変数に入力した値を入れています。

![スクリーンショット 2024-05-29 17.32.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/110a092f-5cd2-6bef-1b21-5faabdcffa97.png)

実際に動かしてみると、入力した都市で天気情報を取得していることがわかります。

## 7. 入力した都市が存在しなかった場合のハンドリング

次に、入力した都市が存在しなかった場合の対応を行います。

![スクリーンショット 2024-05-29 17.36.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/b63a1377-e696-653d-ee69-de8a6beba591.png)

現状、入力した都市が存在していない場合はエラーが発生しますが、画面に表示されないため、ユーザーが認識できない状態となっています。

```Weather.tsx
import axios from "axios";
import { useState } from "react";

const Weather = () => {
  const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
  const [weather, setWeather] = useState<any>(null);
  const [city, setCity] = useState('');
  // 追加
  const [error, setError] = useState<string | null>(null);

  const inputCity = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCity(event.target.value);
  };


  const apiCall = async () => {
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
  
      setWeather(response.data);
      // 追加
      setError(null);
    } catch (error) {
      console.error('Error has occured:', error);
      // 追加
      setWeather(null);
      if (axios.isAxiosError(error) && error.response?.status === 404) {
        setError('入力した都市が見つかりませんでした。');
      } else {
        setError('予期せぬエラーが発生しました。');
      }
    }
  }

  return (
    <div>
      <input
        type="text"
        value={city}
        onChange={inputCity}
        placeholder="都市を入力してください"
      />
      <button onClick={apiCall}>天気を取得</button>
      // 追加
      {error && <p className="error">{error}</p>}
      {weather && !error && (
        <div>
          <h2>{weather.name}</h2>
          <p>Temperature: {weather.main.temp}°C</p>
          <p>Weather: {weather.weather[0].description}</p>
        </div>
      )}
    </div>
  )
}

export default Weather;
```

エラー用のuseStateを用意し、エラーを表示するためのHTMLを追加しています。

```ts
if (axios.isAxiosError(error) && error.response?.status === 404) {
  setError('入力した都市が見つかりませんでした。');
} else {
  setError('予期せぬエラーが発生しました。');
}
```

エラーに関しては、都市が存在しない場合とそれ以外のエラーの2種類で条件分岐しています。エラー文をsetErrorで値を更新し、画面に表示します。

![スクリーンショット 2024-05-29 17.42.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/50dc7c77-971e-4fc2-af13-d73f4f51f1c6.png)



## 8. スタイルを整える

最後にスタイルを整えます。特にこだわりがないようであれば、そのままコピペして使用して大丈夫です。

```Weather.tsx
import axios from "axios";
import { useState } from "react";

const Weather = () => {
  const apiKey = process.env.REACT_APP_OPENWEATHERMAP_API_KEY;
  const [weather, setWeather] = useState<any>(null);
  const [city, setCity] = useState('');
  const [error, setError] = useState<string | null>(null);

  const inputCity = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCity(event.target.value);
  };


  const apiCall = async () => {
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
      );
  
      setWeather(response.data);
      setError(null);
    } catch (error) {
      console.error('Error has occured:', error);
      setWeather(null);
      if (axios.isAxiosError(error) && error.response?.status === 404) {
        setError('入力した都市が見つかりませんでした。');
      } else {
        setError('予期せぬエラーが発生しました。');
      }
    }
  }

  return (
    <div>
      <h1>都市の天気検索</h1>
      <input
        type="text"
        value={city}
        onChange={inputCity}
        placeholder="都市を入力してください"
      />
      <button onClick={apiCall}>天気を取得</button>
      {error && <p>{error}</p>}

      {weather && !error && (
        <div className="weather-info">
          <h2>{weather.name}</h2>
          <p>Temperature: {weather.main.temp}°C</p>
          <p>Weather: {weather.weather[0].description}</p>
        </div>
      )}
    </div>
  )
}

export default Weather;
```

```App.css
body {
  font-family: Arial, sans-serif;
  background-color: #f4f4f9;
  color: #333;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

h1 {
  color: #4CAF50;
}

input[type="text"] {
  padding: 10px;
  margin: 20px 0;
  border: 1px solid #ddd;
  border-radius: 5px;
  width: 200px;
}

button {
  padding: 10px 20px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

button:hover {
  background-color: #45a049;
}

.weather-info {
  background: #e7f5e8;
  margin: 20px 0;
  padding: 20px;
  border-radius: 5px;
  width: 300px;
  text-align: center;
}

.weather-info h2 {
  margin: 0 0 10px 0;
}

.error {
  color: red;
  margin: 20px 0;
}
```

![スクリーンショット 2024-05-31 22.02.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/242816/5ae4403f-6fd6-48aa-5f3a-25dade6647d9.png)

ローカルの画面を見ると、良い感じのデザインになりました。


## おわりに

今回はOpenWeatherMap APIを利用して簡単な天気アプリを実装しました。

天気アプリなどは既存のサービスで充実していますが、実際に自分で作ることでよりサービス作りの解像度を上げることができれば幸いです。

## JISOUのメンバー募集中！
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう！
実践重視：即戦力を育てるアウトプット中心のプログラム。
モダンなスキル : Reactを中心としたモダンな技術を学べる。
キャリアアップ：スキルアップだけでなく、キャリアパスのサポートも充実。
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください！
▼▼▼
https://projisou.jp/