無料APIを用いてお天気アプリをReactで作ってみた（OpenWeatherAPI）

## はじめに
**ゴール**
Reactで外部APIからデータを取得して表示するアプリケーションの作り方を学べる。
**概要**
OpenWeather APIを使用して天気情報を取得し、シンプルな天気アプリを作成する。機能は以下。
- 都市名を入力すると、その都市の天気情報を取得
- 温度をセルシウス度で表示
- 天気に応じたアイコンの表示

完成形はこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/7834950a-b460-4dbc-87ef-f2596e88d464.png)

## 用いている技術要素

|技術|説明|
|---|---|
|React|UIコンポーネントを構築するためのJavaScriptライブラリ|
|Material-UI (MUI)|Reactのコンポーネントライブラリで、美しいUIを簡単に実装可能|
|Axios|HTTPリクエストを簡単に行えるライブラリ|
|Weather Icons React|天気アイコンを表示するためのコンポーネントライブラリ|

## OpenWeather APIについて

OpenWeather APIは、世界中の都市の天気情報を提供する人気のある気象データサービス。このAPIを使用することで、現在の気温や天気の状態（晴れ、曇り、雨など）、湿度等様々な情報が取得できる。

APIの使用には無料のAPIキーが必要で、1分あたり60回までのリクエストが可能です。このチュートリアルでは、現在の天気を取得するエンドポイント（`/data/2.5/weather`）を使用します。
下記の記事を参考に、APIを取得してからプロジェクトの作成に取り掛かってください。

https://zenn.dev/daifukuninja/articles/5e696cd0a75ba8

## ステップ 1: プロジェクトのセットアップ

まず、必要なパッケージをインストールします：

```bash
npx create-react-app weather-app
cd weather-app
npm install @mui/material @emotion/react @emotion/styled axios weather-icons-react
```

プロジェクトの構成は以下のようになります：

```
weather-app/
├── src/
│   ├── App.js
│   └── comps/
│       └── WiComp.js
└── package.json
```

## ステップ 2: MUIによるTextFieldの実装

以下のコードを`src/App.js`に実装します：

```jsx
import React, { useState } from "react";
import { Container, Box, TextField, Typography } from "@mui/material";

export default function App() {
  const [city, setCity] = useState("");

  return (
    <Container maxWidth="sm">
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 3,
          my: 4,
        }}
      >
        <Typography variant="h4" component="h1">
          天気情報取得アプリ
        </Typography>

        <TextField
          fullWidth
          label="都市名を入力"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          variant="outlined"
          sx={{ maxWidth: 400 }}
        />

        {/* 入力値の確認用表示 */}
        <Typography variant="body1">入力値: {city}</Typography>
      </Box>
    </Container>
  );
}
```

とりあえず入力した文字が出力できるようになった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/a9e59c54-8c70-46f3-878e-274ed5eddb02.png)


## ステップ 3: OpenWeather APIによる天候情報の取得

```jsx
// 【更新】importを適切に更新
import React, { useState, useEffect } from "react";
import { Container, Box, TextField, Typography } from "@mui/material";
import axios from "axios";

// 【追記】あなたのAPIキーに置き換えてください
const API_KEY = "xxxxxxxxxxxxxxxxxxxxx";

export default function App() {
  const [city, setCity] = useState("");
  // 【追記】状態変数を追加
  const [weather, setWeather] = useState(null);

  // 【追記】city が更新されるたびに呼び出される
  useEffect(() => {
    const fetchData = async () => {
      if (!city) return;
      try {
        const response = await axios.get(
          `https://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${API_KEY}`
        );
        console.log("取得した天候データ:", response.data);
        setWeather(response.data);
      } catch (error) {
        console.error("天候情報の取得に失敗しました:", error);
      }
    };

    fetchData();
  }, [city]);

  return (
    <Container maxWidth="sm">
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 3,
          my: 4,
        }}
      >
        <Typography variant="h4" component="h1">
          天気情報取得アプリ
        </Typography>

        <TextField
          fullWidth
          label="都市名を入力"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          variant="outlined"
          sx={{ maxWidth: 400 }}
        />

        {/* 【追記】取得した天候データを簡易的に表示 */}
        {weather && (
          <Box>
            <Typography variant="h6">都市名: {weather.name}</Typography>
            <Typography variant="body1">
              温度: {weather.main.temp} K
            </Typography>
          </Box>
        )}
      </Box>
    </Container>
  );
}
```
`Tokyo`と試しに打つと、以下のように取得できていることがわかる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/06d488b4-98a5-4094-86d3-c505fce5f276.png)



## ステップ 4: エラーハンドリングの実装

```jsx
import React, { useState, useEffect } from "react";
import { Container, Box, TextField, Typography } from "@mui/material";
import axios from "axios";

const API_KEY = "xxxxxxxxxxxxxxxxxxxxx";

export default function App() {
  const [city, setCity] = useState("");
  const [weather, setWeather] = useState(null);
  // 【追記】状態変数を追加
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      if (!city) return;
      try {
        const response = await axios.get(
          `https://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${API_KEY}`
        );
        setWeather(response.data);
        setError("");
      } catch (error) {
        // 【追記】エラー時に適切な内容を変数（error）に格納
        if (error.response && error.response.status === 404) {
          setError("データが見つかりませんでした。");
        } else {
          setError("データの取得に失敗しました");
        }
        setWeather(null);
      }
    };

    fetchData();
  }, [city]);

  return (
    <Container maxWidth="sm">
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 3,
          my: 4,
        }}
      >
        <Typography variant="h4" component="h1">
          天気情報取得アプリ
        </Typography>

        <TextField
          fullWidth
          label="都市名を入力"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          variant="outlined"
          sx={{ maxWidth: 400 }}
        />

        {/* 【追記】エラー表示 */}
        {error && (
          <Typography color="error" sx={{ mt: 2 }}>
            {error}
          </Typography>
        )}

        {weather && (
          <Box>
            <Typography variant="h6">都市名: {weather.name}</Typography>
            <Typography variant="body1">
              温度: {weather.main.temp} K
            </Typography>
          </Box>
        )}
      </Box>
    </Container>
  );
}
```

これで、検索した文言が見つからない場合はエラー表示となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/1cd1d90b-1640-46fc-8014-5c76653b488d.png)


## ステップ 5: 取得した情報の画面出力

```jsx
import React, { useState, useEffect } from "react";
// 【更新】importを適切に更新
import {
  Container,
  Box,
  TextField,
  Typography,
  Card,
  CardContent,
} from "@mui/material";
import axios from "axios";

const API_KEY = "xxxxxxxxxxxxxxxxxxxxx";

export default function App() {
  const [city, setCity] = useState("");
  const [weather, setWeather] = useState(null);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      if (!city) return;
      try {
        const response = await axios.get(
          `https://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${API_KEY}`
        );
        setWeather(response.data);
        setError("");
      } catch (error) {
        if (error.response && error.response.status === 404) {
          setError("データが見つかりませんでした。");
        } else {
          setError("データの取得に失敗しました");
        }
        setWeather(null);
      }
    };

    fetchData();
  }, [city]);

  return (
    <Container maxWidth="sm">
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 3,
          my: 4,
        }}
      >
        <Typography
          variant="h4"
          component="h1"
          sx={{ fontWeight: "bold", color: "primary.main", mb: 2 }}
        >
          天気情報取得アプリ
        </Typography>

        <TextField
          fullWidth
          label="都市名を入力"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          variant="outlined"
          sx={{ maxWidth: 400 }}
        />

        {error && (
          <Typography color="error" sx={{ mt: 2 }}>
            {error}
          </Typography>
        )}

        {/* 【更新】カードのデザインを整える */}
        {weather && (
          <Card
            sx={{
              width: "100%",
              maxWidth: 400,
              boxShadow: 3,
              borderRadius: 2,
            }}
          >
            <CardContent
              sx={{
                display: "flex",
                flexDirection: "column",
                alignItems: "center",
                gap: 2,
              }}
            >
              <Typography variant="h5" component="h2">
                {weather.name}
              </Typography>
              <Typography variant="h6">
                {Math.round((weather.main.temp - 273.15) * 10) / 10} °C
              </Typography>
            </CardContent>
          </Card>
        )}
      </Box>
    </Container>
  );
}
```
Cardコンポーネントにより、体裁が整った。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/f00a5776-9f44-4571-bcbe-c4f2a4e03082.png)



## ステップ 6: 天候アイコンの外部コンポーネント化

まず、`src/comps/WiComp.js`を作成します：

```jsx
import React from "react";
import { Box } from "@mui/material";
import { WiCloud, WiDaySunny, WiRain, WiSnowWind } from "weather-icons-react";

export default function WiComp({ weather_type }) {
  const getWeatherIcon = () => {
    switch (weather_type) {
      case "Clouds":
        return <WiCloud size={80} color="#607d8b" />;
      case "Snow":
        return <WiSnowWind size={80} color="#90a4ae" />;
      case "Rain":
        return <WiRain size={80} color="#4fc3f7" />;
      default:
        return <WiDaySunny size={80} color="#ffd54f" />;
    }
  };

  return <Box sx={{ py: 1 }}>{getWeatherIcon()}</Box>;
}
```

そして、最終的な`App.js`は以下のようになります：

```jsx
import React, { useState, useEffect } from "react";
import {
  Container,
  Box,
  TextField,
  Typography,
  Card,
  CardContent,
} from "@mui/material";
import axios from "axios";
// 【追記】importを追記
import WiComp from "./comps/WiComp";

const API_KEY = "xxxxxxxxxxxxxxxxxxxxx";

export default function App() {
  const [city, setCity] = useState("");
  const [weather, setWeather] = useState(null);
  const [error, setError] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      if (!city) return;
      try {
        const response = await axios.get(
          `https://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${API_KEY}`
        );
        setWeather(response.data);
        setError("");
      } catch (error) {
        if (error.response && error.response.status === 404) {
          setError("データが見つかりませんでした。");
        } else {
          setError("データの取得に失敗しました");
        }
        setWeather(null);
      }
    };

    fetchData();
  }, [city]);

  return (
    <Container maxWidth="sm">
      <Box
        sx={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 3,
          my: 4,
        }}
      >
        <Typography
          variant="h4"
          component="h1"
          sx={{ fontWeight: "bold", color: "primary.main", mb: 2 }}
        >
          天気情報取得アプリ
        </Typography>

        <TextField
          fullWidth
          label="都市名を入力"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          variant="outlined"
          sx={{ maxWidth: 400 }}
        />

        {error && (
          <Typography color="error" sx={{ mt: 2 }}>
            {error}
          </Typography>
        )}

        {weather && (
          <Card
            sx={{
              width: "100%",
              maxWidth: 400,
              boxShadow: 3,
              borderRadius: 2,
            }}
          >
            <CardContent
              sx={{
                display: "flex",
                flexDirection: "column",
                alignItems: "center",
                gap: 2,
              }}
            >
              <Typography variant="h5" component="h2">
                {weather.name}
              </Typography>
              {/* 【追記】WiCompコンポーネントを呼び出す */}
              <WiComp weather_type={weather.weather[0].main} />
              <Typography variant="h6">
                {Math.round((weather.main.temp - 273.15) * 10) / 10} °C
              </Typography>
            </CardContent>
          </Card>
        )}
      </Box>
    </Container>
  );
}
```
アイコンが表示されたらOK.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/7834950a-b460-4dbc-87ef-f2596e88d464.png)


ちなみに、NewYorkは雨。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628846/7e8cbe66-2692-4fac-aaa7-f0549bf96f96.png)

これで完成

## 注意点

- OpenWeather APIのAPI_KEYは必ず自身のものに置き換えてください
- 必要なパッケージが全てインストールされていることを確認してください
- コンポーネントファイルの配置場所や名前は、プロジェクトの構成に合わせて適宜調整してください