
JSXの書き方:React開発の第一歩
JSX(JavaScript XML)はReactの核となる構文で、JavaScript内にHTMLのようなマークアップを直接記述できるようにします。これにより […]
この記事では、React初学者向けの天気アプリ演習の解答例と、API連携における重要なポイントを詳しく解説します。外部APIとの連携という実践的なスキルを、コードを通じて学んでいきましょう。
完成形のコード全体(src/App.js
):
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
function App() {
// Stateの宣言
const [city, setCity] = useState('');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [unit, setUnit] = useState('metric'); // 温度単位(metric:℃, imperial:℉)
// APIキー(実際には環境変数などで管理するのがベストプラクティス)
const API_KEY = 'YOUR_API_KEY';
// 天気データ取得関数
const fetchWeather = async (cityName) => {
if (!cityName) return;
setLoading(true);
setError('');
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${API_KEY}&units=${unit}&lang=ja`
);
setWeather(response.data);
} catch (err) {
setError('都市が見つかりませんでした。正しい都市名を入力してください');
setWeather(null);
console.error('APIエラー:', err);
} finally {
setLoading(false);
}
};
// 初期表示時に東京の天気を取得
useEffect(() => {
fetchWeather('Tokyo');
}, []); // 空の依存配列 = 初回レンダリング時のみ実行
// 温度単位が変更されたら再取得
useEffect(() => {
if (weather) {
fetchWeather(city || weather.name);
}
}, [unit]);
// 温度単位を切り替える関数
const toggleUnit = () => {
setUnit(unit === 'metric' ? 'imperial' : 'metric');
};
// 天気アイコンを取得する関数
const getWeatherIcon = (iconCode) => {
return `http://openweathermap.org/img/wn/${iconCode}@2x.png`;
};
// 温度表示をフォーマットする関数
const formatTemp = (temp) => {
return unit === 'metric' ? `${Math.round(temp)}°C` : `${Math.round(temp)}°F`;
};
// 風速表示をフォーマットする関数
const formatWindSpeed = (speed) => {
return unit === 'metric' ? `${speed} m/s` : `${(speed * 2.237).toFixed(1)} mph`;
};
return (
<div className="app">
<h1>天気予報アプリ</h1>
{/* 検索フォーム */}
<div className="search-container">
<div className="search-form">
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="都市名を入力(例: Osaka)"
onKeyPress={(e) => e.key === 'Enter' && fetchWeather(city)}
/>
<button
onClick={() => fetchWeather(city)}
disabled={loading || !city.trim()}
>
{loading ? '取得中...' : '検索'}
</button>
</div>
{/* 温度単位切り替えボタン */}
<button className="unit-toggle" onClick={toggleUnit}>
{unit === 'metric' ? '℉ 表示' : '℃ 表示'}
</button>
</div>
{/* エラーメッセージ */}
{error && <div className="error-message">{error}</div>}
{/* 天気情報表示 */}
{weather && (
<div className="weather-container">
<div className="weather-header">
<h2>
{weather.name}, {weather.sys.country}
<span className="current-date">
{new Date().toLocaleDateString('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short'
})}
</span>
</h2>
</div>
<div className="weather-main">
<div className="weather-primary">
<img
src={getWeatherIcon(weather.weather[0].icon)}
alt={weather.weather[0].description}
/>
<div className="weather-temp">
{formatTemp(weather.main.temp)}
<span className="weather-description">
{weather.weather[0].description}
</span>
</div>
</div>
<div className="weather-details">
<div className="detail-item">
<span className="detail-label">体感温度</span>
<span className="detail-value">{formatTemp(weather.main.feels_like)}</span>
</div>
<div className="detail-item">
<span className="detail-label">湿度</span>
<span className="detail-value">{weather.main.humidity}%</span>
</div>
<div className="detail-item">
<span className="detail-label">気圧</span>
<span className="detail-value">{weather.main.pressure} hPa</span>
</div>
<div className="detail-item">
<span className="detail-label">風速</span>
<span className="detail-value">{formatWindSpeed(weather.wind.speed)}</span>
</div>
<div className="detail-item">
<span className="detail-label">最低気温</span>
<span className="detail-value">{formatTemp(weather.main.temp_min)}</span>
</div>
<div className="detail-item">
<span className="detail-label">最高気温</span>
<span className="detail-value">{formatTemp(weather.main.temp_max)}</span>
</div>
</div>
</div>
</div>
)}
{/* クレジット表示 */}
<div className="credit">
<p>Powered by <a href="https://openweathermap.org/" target="_blank" rel="noopener noreferrer">OpenWeatherMap</a></p>
</div>
</div>
);
}
export default App;
const [city, setCity] = useState('');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [unit, setUnit] = useState('metric');
city
: 検索する都市名weather
: APIから取得した天気データloading
: データ取得中の状態error
: エラーメッセージunit
: 温度単位(℃/℉)// 初期表示時に東京の天気を取得
useEffect(() => {
fetchWeather('Tokyo');
}, []);
// 温度単位が変更されたら再取得
useEffect(() => {
if (weather) {
fetchWeather(city || weather.name);
}
}, [unit]);
unit
を依存配列に指定)const fetchWeather = async (cityName) => {
if (!cityName) return;
setLoading(true);
setError('');
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${API_KEY}&units=${unit}&lang=ja`
);
setWeather(response.data);
} catch (err) {
setError('都市が見つかりませんでした。正しい都市名を入力してください');
setWeather(null);
console.error('APIエラー:', err);
} finally {
setLoading(false);
}
};
async/await
構文を使用した非同期処理loading
状態をtrue
にweather
状態に保存finally
で常にloading
状態をfalse
に// 検索フォーム
onKeyPress={(e) => e.key === 'Enter' && fetchWeather(city)}
// ボタンの無効化
disabled={loading || !city.trim()}
// ローディング表示
{loading ? '取得中...' : '検索'}
// 温度表示
const formatTemp = (temp) => {
return unit === 'metric' ? `${Math.round(temp)}°C` : `${Math.round(temp)}°F`;
};
// 日付表示
{new Date().toLocaleDateString('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short'
})}
src/App.css
の内容例:
/* 基本スタイル */
body {
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
}
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
}
/* 検索フォーム */
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.search-form {
display: flex;
flex-grow: 1;
}
.search-form input {
flex-grow: 1;
padding: 12px 15px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
outline: none;
transition: border-color 0.3s;
}
.search-form input:focus {
border-color: #3498db;
}
.search-form button {
padding: 0 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s;
}
.search-form button:hover {
background-color: #2980b9;
}
.search-form button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.unit-toggle {
padding: 12px 15px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.3s;
}
.unit-toggle:hover {
background-color: #27ae60;
}
/* エラーメッセージ */
.error-message {
padding: 10px;
background-color: #ffecec;
color: #e74c3c;
border-radius: 4px;
margin-bottom: 20px;
text-align: center;
}
/* 天気情報コンテナ */
.weather-container {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.weather-header {
margin-bottom: 20px;
text-align: center;
}
.weather-header h2 {
margin: 0;
color: #2c3e50;
font-size: 24px;
}
.current-date {
display: block;
font-size: 14px;
color: #7f8c8d;
margin-top: 5px;
font-weight: normal;
}
.weather-main {
display: flex;
flex-direction: column;
gap: 20px;
}
.weather-primary {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
}
.weather-primary img {
width: 100px;
height: 100px;
}
.weather-temp {
font-size: 48px;
font-weight: bold;
color: #2c3e50;
}
.weather-description {
display: block;
font-size: 16px;
font-weight: normal;
text-transform: capitalize;
color: #34495e;
}
.weather-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.detail-item {
background-color: rgba(255, 255, 255, 0.7);
padding: 10px;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
}
.detail-label {
font-size: 12px;
color: #7f8c8d;
margin-bottom: 5px;
}
.detail-value {
font-size: 16px;
font-weight: bold;
color: #2c3e50;
}
/* クレジット */
.credit {
margin-top: 30px;
text-align: center;
font-size: 12px;
color: #7f8c8d;
}
.credit a {
color: #3498db;
text-decoration: none;
}
.credit a:hover {
text-decoration: underline;
}
/* レスポンシブ対応 */
@media (max-width: 480px) {
.search-container {
flex-direction: column;
}
.search-form {
width: 100%;
}
.unit-toggle {
width: 100%;
}
.weather-details {
grid-template-columns: 1fr;
}
}
この天気アプリを通じて学ぶべきReactとAPI連携の重要な概念:
A: 実際のプロダクション環境では、APIキーを直接コードに書くのはセキュリティ上危険です。.env
ファイルを使用して環境変数として管理するのがベストプラクティスです。
REACT_APP_WEATHER_API_KEY=your_api_key_here
const API_KEY = process.env.REACT_APP_WEATHER_API_KEY;
A: fetchでも可能ですが、axiosには以下の利点があります:
A: useEffectの第二引数に渡す配列で、ここに指定した値が変更された場合のみeffectが再実行されます。空の配列を渡すと初回レンダリング時のみ実行されます。
A: 副作用(API通信)はレンダリング中に直接実行すべきではなく、useEffect内で行うのがReactの推奨パターンです。これにより、レンダリングのパフォーマンスが向上し、予期しない副作用を防げます。
useEffect(() => {
navigator.geolocation?.getCurrentPosition((position) => {
fetchWeatherByCoords(position.coords.latitude, position.coords.longitude);
});
}, []);
const fetchForecast = async (cityName) => {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${cityName}&appid=${API_KEY}&units=${unit}&lang=ja`
);
setForecast(response.data.list);
};
const [searchHistory, setSearchHistory] = useState([]);
const addToHistory = (city) => {
setSearchHistory(prev => [...new Set([city, ...prev])].slice(0, 5));
};
const [theme, setTheme] = useState('light');
// 天気データから昼夜を判断
useEffect(() => {
if (weather) {
const isDayTime = weather.weather[0].icon.includes('d');
setTheme(isDayTime ? 'light' : 'dark');
}
}, [weather]);
この天気アプリの演習を通じて、ReactにおけるAPI連携の基本パターンを学びました。特に重要なのは:
次のステップとして、より複雑なAPI連携(認証が必要なAPI)、データのキャッシュ(React Queryなどのライブラリ)、または複数APIを組み合わせたアプリケーション開発に挑戦してみると良いでしょう。