1. はじめに

デザイン事業部の長谷です。

今回は、「天気検索アプリの作成を通し、ReactやAPIへの理解を深めよう」の後編になります!

前編では、Viteを使用した開発環境の準備〜OpenWeatherMapのAPIキー取得、Reactの組み込みHooksを使用したデータ取得の実装について学びました。

先に前編をご覧いただいた方が、後編の内容がスムーズに入るかと思うので
まだご覧になられていない方は、前編もご覧下さい!

後編では、取得した天気データを表示するためのUIをMaterial-UI(MUI)とBulmaを使用し、簡単に作成していきます。

また今回アプリを作成しているのは、あくまでReactやAPIへの理解を深めるためになります。従って、「素早く・簡単」に作成するUIになりますので、ご容赦ください。

2. Material-UI(MUI)の導入

Material-UI、通称MUIは、Reactのためのデザインコンポーネントライブラリです。
複雑なUIを実現する場合には、難点が出てくるかもしれませんが、今回は「素早く・簡単」にが最重要ですので、その点MUIは優れていると思います。

以下が今回の手順になります。
1.MUIをインストールします。
(今回はMUIのアイコンのみ使用する為、この手順は飛ばしても、実装はできると思います)

1
npm install @mui/material @emotion/react @emotion/styled

2.MUIのアイコンをインストールします。

1
npm install @mui/icons-material

3.MUIのコンポーネントを使用し、天気アイコンを表示します。
App.jsxの中に以下のように、記述します。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { useState, useEffect } from 'react';
// 以下文で使用するMUIのアイコンをインポート
import { Search, WbSunny, WbCloudy, Opacity, ThunderstormTwoTone, GrainTwoTone } from '@mui/icons-material';
 
const WeatherApp = () => {
  // 状態管理用のuseState
  const [city, setCity] = useState('');
 
  // 都市名が変更された際に天気データを取得するためのuseEffect
  useEffect(() => {});
 
  // 天気状態に応じたアイコンを返す関数
  const getWeatherIcon = (weatherCondition) => {
    const iconMap = {
      Clear: <WbSunny fontSize="large" />,
      Clouds: <WbCloudy fontSize="large" />,
      Rain: <Opacity fontSize="large" />,
      Drizzle: <GrainTwoTone fontSize="large" />,
      Thunderstorm: <ThunderstormTwoTone fontSize="large" />,
    };
    return iconMap[weatherCondition] || null;
  };
 
 
  return (
    <div className="container">
      {/* 検索バー */}
      <div className="is-flex is-justify-content-center is-align-items-center mb-2">
        <Search fontSize="medium" />
        <h1 className="title is-6 pr-2">地域検索</h1>
      </div>
      {/* 天気情報 */}
      {weatherData && (
        <div className="box">
          <div>
            {getWeatherIcon(weatherData.weather[0].main)}
            <p className="heading is-size-7">{weatherData.weather[0].description}</p>
          </div>
        </div>
      )}
    </div>
  );
};
 
export default WeatherApp;

上記のコードでは、weatherConditionに応じて、適切な天気アイコンを表示しています。

3. Bulmaの導入

MUIのアイコンを無事使用できるようになったので、次はスタイルを当てていきます。
今回は「素早く・簡単」を求めるため、Bulmaを使用していきます。

ここで、Bulmaについて軽く説明します。
Bulmaとは、シンプルで使いやすいCSSフレームワークです。他の同種で例を挙げると、BootstrapやTailwind CSSがあります。これらのCSSフレームワークは基本的に、「読み込み・定義されているクラス名を付与」という一連の手段をとることで、クラス名に紐づいているスタイルを付与できます。

それぞれに特徴があり、Reactとの相性を考えると、実務で使用するのであれば、ReactとTailwind CSSの組み合わせがベストな気がします。しかし、今回は見栄えが悪くなりすぎない最低限のUIを実装できればいいので、手軽に利用できるBulmaを使用して、スタイルを付与していきます。

以下が具体的な手順です。
1.Bulmaをインストールします。

1
npm install bulma

2.Bulmaのスタイルシートをインポートします。
App.jsxの中に、以下の文を記述します。

1
import 'bulma/css/bulma.min.css';

3.Bulmaのクラスを使用し、天気情報を表示するコンポーネントを作成する。
App.jsxの中に、以下のように記述します。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from 'react';
import 'bulma/css/bulma.min.css';
 
const WeatherApp = () => {
 return (
   <div className="container">
      {/* 検索バー */}
      <div className="is-flex is-justify-content-center is-align-items-center mb-2">
        <Search fontSize="medium" />
        <h1 className="title is-6 pr-2">地域検索</h1>
      </div>
      <div className="field">
        <div className="control">
          <input
            className="input"
            type="text"
            placeholder="地域を入力"
            value={city}
            onChange={(e) => setCity(e.target.value)}
          />
        </div>
      </div>
      {/* エラーメッセージ */}
      {error && <p className="help is-danger">{error}</p>}
      {/* 天気情報 */}
      {weatherData && (
        <div className="box">
          <h2 className="subtitle is-5 has-text-centered mb-2">{weatherData.name}</h2>
          <p className="is-size-6 has-text-centered mb-3">{formatDateTime(weatherData.forecast.list[0].dt_txt)}</p>
          <div className="level is-mobile mb-5">
            <div className="level-item has-text-centered">
              <div>
                {getWeatherIcon(weatherData.weather[0].main)}
                <p className="heading is-size-7">{weatherData.weather[0].description}</p>
              </div>
            </div>
            <div className="level-item has-text-centered">
              <p className="title is-4">{weatherData.main.temp}<span className="is-size-6">°C</span></p>
            </div>
          </div>
          <div className="level is-mobile">
            <div className="level-item has-text-centered">
              <div>
                <p className="title is-6">
                  <span className="has-text-danger">{weatherData.forecast.list[0].main.temp_max}°C</span> / <span className="has-text-info">{weatherData.forecast.list[0].main.temp_min}°C</span>
                </p>
              </div>
            </div>
            <div className="level-item has-text-centered">
              <div>
                <p className="title is-6 is-flex is-align-items-center">
                  <Opacity fontSize="small" /> {weatherData.forecast.list[0].pop * 100}%
                </p>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
 );
}
 
export default WeatherInfo;

上記のコードでは、Bulmaのクラスを使用して、都市名、天気アイコン、気温、天気の説明を表示しています。

4. コンポーネントの実装

最後に、検索バーと天気情報を表示するコンポーネントを組み合わせて、アプリを完成させます。

以下が最終的なApp.jsxのコードになります。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Search, WbSunny, WbCloudy, Opacity, ThunderstormTwoTone, GrainTwoTone } from '@mui/icons-material';
import 'bulma/css/bulma.min.css';
 
const API_KEY = 'MY_API_KEY';
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
const FORECAST_URL = 'https://api.openweathermap.org/data/2.5/forecast';
 
const WeatherApp = () => {
  // 状態管理用のuseState
  const [city, setCity] = useState('');
  const [weatherData, setWeatherData] = useState(null);
  const [error, setError] = useState(null);
 
  // 都市名が変更された際に天気データを取得するためのuseEffect
  useEffect(() => {
    const fetchData = async () => {
      // 都市名が空の場合は処理を終了
      if (!city) return;
 
      try {
        // 現在の天気データと予報データを並行して取得
        const [weatherResponse, forecastResponse] = await Promise.all([
          axios.get(BASE_URL, { params: { q: city, appid: API_KEY, units: 'metric', lang: 'ja' } }),
          axios.get(FORECAST_URL, { params: { q: city, appid: API_KEY, units: 'metric', lang: 'ja' } }),
        ]);
 
        // 取得したデータを組み合わせてweatherDataに設定
        setWeatherData({ ...weatherResponse.data, forecast: forecastResponse.data });
        setError(null);
      } catch (error) {
        // エラーが発生した場合はweatherDataをnullに設定し、エラーメッセージを表示
        setWeatherData(null);
        setError('都市名を正しく入力して下さい。');
      }
    };
 
    fetchData();
  }, [city]);
 
  // 天気状態に応じたアイコンを返す関数
  const getWeatherIcon = (weatherCondition) => {
    const iconMap = {
      Clear: <WbSunny fontSize="large" />,
      Clouds: <WbCloudy fontSize="large" />,
      Rain: <Opacity fontSize="large" />,
      Drizzle: <GrainTwoTone fontSize="large" />,
      Thunderstorm: <ThunderstormTwoTone fontSize="large" />,
    };
    return iconMap[weatherCondition] || null;
  };
 
  // 日時データを日本語表記にフォーマットする関数
  const formatDateTime = (dateTimeString) => {
    return new Date(dateTimeString).toLocaleString('ja-JP', { month: 'long', day: 'numeric' });
  };
 
  return (
    <div className="container">
      {/* 検索バー */}
      <div className="is-flex is-justify-content-center is-align-items-center mb-2">
        <Search fontSize="medium" />
        <h1 className="title is-6 pr-2">地域検索</h1>
      </div>
      <div className="field">
        <div className="control">
          <input
            className="input"
            type="text"
            placeholder="地域を入力"
            value={city}
            onChange={(e) => setCity(e.target.value)}
          />
        </div>
      </div>
      {/* エラーメッセージ */}
      {error && <p className="help is-danger">{error}</p>}
      {/* 天気情報 */}
      {weatherData && (
        <div className="box">
          <h2 className="subtitle is-5 has-text-centered mb-2">{weatherData.name}</h2>
          <p className="is-size-6 has-text-centered mb-3">{formatDateTime(weatherData.forecast.list[0].dt_txt)}</p>
          <div className="level is-mobile mb-5">
            <div className="level-item has-text-centered">
              <div>
                {getWeatherIcon(weatherData.weather[0].main)}
                <p className="heading is-size-7">{weatherData.weather[0].description}</p>
              </div>
            </div>
            <div className="level-item has-text-centered">
              <p className="title is-4">{weatherData.main.temp}<span className="is-size-6">°C</span></p>
            </div>
          </div>
          <div className="level is-mobile">
            <div className="level-item has-text-centered">
              <div>
                <p className="title is-6">
                  <span className="has-text-danger">{weatherData.forecast.list[0].main.temp_max}°C</span> / <span className="has-text-info">{weatherData.forecast.list[0].main.temp_min}°C</span>
                </p>
              </div>
            </div>
            <div className="level-item has-text-centered">
              <div>
                <p className="title is-6 is-flex is-align-items-center">
                  <Opacity fontSize="small" /> {weatherData.forecast.list[0].pop * 100}%
                </p>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};
 
export default WeatherApp;

上記のコードでは、検索バーと天気情報を表示するコンポーネントを組み合わせています。handleCityChange関数で、入力された都市名をcityの状態に設定し、useEffect内でデータ取得を行っています。

また、App.jsx内に付与したBulmaのスタイルだけだと要素が左右中央に配置されないと思うので、既存のindex.cssファイル内に申し訳程度で以下コードを書き足していただければ、完成です。
index.cssの中にも、以下スタイルを追記します。

1
2
3
4
5
/* 〜既存のコード〜 */
 
#root {
  margin-inline: auto; /* 「margin: 0 auto」でもOK */
}

5. アプリの完成

これで、簡単な天気検索アプリが完成しました。アプリを起動すると、以下のような画面が表示されます。
完成したアプリのファーストビュー

実際の使用イメージは以下になります。
完成したアプリの動作

6. まとめ

前後編を含め、本記事ではReactとOpenWeatherMap APIを使用して、簡単な天気検索アプリを作成する方法を紹介しました。いかがでしたでしょうか?

個人的にViteを使用した開発環境の準備、APIキーの取得、データ取得の実装、MUIとBulmaを使用したUIデザインについて学ぶことができました。

今後も、React・APIを使用して、ウェブアプリケーション開発の理解を深めていこうと思います。

本記事が誰かの役に立てば幸いです。
読んでいただきありがとうございました!