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のアイコンのみ使用する為、この手順は飛ばしても、実装はできると思います)

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

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

npm install @mui/icons-material

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

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をインストールします。

npm install bulma

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

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

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

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のコードになります。

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の中にも、以下スタイルを追記します。

/* 〜既存のコード〜 */

#root {
  margin-inline: auto; /* 「margin: 0 auto」でもOK */
}

5. アプリの完成

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

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

6. まとめ

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

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

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

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