Урок 16: Создание приложения «Погода» в React

Сегодня у нас настал тот самый момент, когда теория встречается с практикой. Мы прошли долгий путь, изучили JSX, состояние, хуки, работу с формами и событиями. Теперь пришло время объединить все эти знания в одном красивом и функциональном проекте. В этом уроке мы с вами создадим приложение для отображения погоды, которое будет получать актуальные данные из внешнего API.

Этот проект станет настоящим прорывом в вашем обучении. Мы создадим полноценное мини-приложение, которое взаимодействует с реальным миром. Вы на собственном опыте почувствуете, как все изученные ранее концепции складываются в единую картину. Мы будем использовать useState для управления состоянием, useEffect для выполнения side-эффектов (в нашем случае API-запросов), обрабатывать пользовательский ввод и динамически отображать данные. Это тот самый опыт, который превращает начинающего разработчика в уверенного создателя интерфейсов.

План нашего приложения

Прежде чем бросаться писать код, давайте четко определим, что именно мы хотим получить в итоге. Любой успешный проект начинается с качественного планирования. Наше приложение «Погода» будет иметь следующий функционал, поле для ввода названия города, кнопку для отправки запроса и область для отображения полученных данных. Данные, которые мы хотим видеть, включают в себя: город и страну, текущую температуру, краткое описание погоды (например, «облачно»), иконку, визуализирующую эту погоду, а также дополнительные параметры, такие как влажность и скорость ветра.

Для получения этих данных мы будем использовать бесплатный и популярный API OpenWeatherMap. Это отличный пример того, как современные веб-приложения взаимодействуют с серверами. Вы отправляете запрос на конкретный URL-адрес с определенными параметрами, а в ответ получаете структурированные данные в формате JSON. Наша задача на React, корректно отправить этот запрос, дождаться ответа, обработать возможные ошибки (например, если город не найден) и красиво отобразить полученную информацию в интерфейсе. Давайте мысленно разобьем наше приложение на компоненты: главный App, компонент поиска SearchBox и компонент для отображения погоды WeatherCard.

Получаем API-ключ от OpenWeatherMap

Первым делом нам нужно зарегистрироваться на сайте OpenWeatherMap (https://openweathermap.org/api) и получить бесплатный API-ключ. Этот ключ является вашим уникальным идентификатором, который вы должны передавать с каждым запросом. Без него сервер не выдаст вам никаких данных. Процесс регистрации простой и займет всего пару минут. После подтверждения email вы найдете свой ключ в личном кабинете, в разделе «API Keys». Сохраните его в надежном месте, но помните, что в реальных проектах для фронтенда часто используются специальные техники для защиты таких ключей (например, прокси-сервер), так как в коде, который работает в браузере пользователя, они могут быть видны.

Для нашего учебного проекта мы будем хранить ключ прямо в коде, но как только вы начнете работать над более серьезными приложениями, обязательно изучите тему переменных окружения. В Create React App для этого можно создать файл .env.local в корне проекта и поместить туда ключ в формате REACT_APP_WEATHER_API_KEY=ваш_ключ_здесь. Затем в коде вы сможете обращаться к нему как process.env.REACT_APP_WEATHER_API_KEY. Это более безопасная практика, особенно если вы планируете выкладывать код в открытый доступ на GitHub.

Создаем структуру компонентов и состояние

Давайте начнем писать код. Мы создадим новый проект или откроем существующий. Внутри нашего главного компонента App мы определим состояние, используя хук useState. Какие данные нам нужно хранить? Во-первых, это сами данные о погоде, которые мы получим от API. Во-вторых, это значение из поля ввода (название города). И в-третьих, нам понадобится состояние для отслеживания загрузки и возможных ошибок.

jsx
import React, { useState } from 'react';
import './App.css';

function App() {
  // Состояние для хранения данных о погоде
  const [weatherData, setWeatherData] = useState(null);
  // Состояние для хранения названия города из input
  const [city, setCity] = useState('');
  // Состояние для отслеживания процесса загрузки
  const [loading, setLoading] = useState(false);
  // Состояние для хранения сообщения об ошибке
  const [error, setError] = useState('');

  // Здесь позже будет функция для выполнения API-запроса

  return (
    <div className="app">
      <h1>Прогноз погоды</h1>
      {/* Поле поиска и кнопка */}
      {/* Место для отображения погоды, загрузки или ошибки */}
    </div>
  );
}

export default App;

Как видите, мы подготовили «контейнеры» для всех необходимых данных. Состояние weatherData изначально null, потому что данных у нас еще нет. city пустая строка, loading — false (загрузка не идет), а error пустая строка (ошибок нет). Эта структура состояния является типичной для компонента, который работает с асинхронными запросами.

Создаем форму для поиска и обрабатываем ввод пользователя

Теперь создадим пользовательский интерфейс для ввода города. Это будет простая форма с полем ввода и кнопкой. Мы будем использовать контролируемый компонент input, как мы это изучали в уроке по формам. Это значит, что значение input привязано к состоянию city, а каждое изменение этого input обновляет состояние.

jsx
// ... внутри return компонента App
<div className="search-box">
  <input
    type="text"
    placeholder="Введите город..."
    value={city}
    onChange={(e) => setCity(e.target.value)}
  />
  <button onClick={/*здесь будет функция запроса*/}>
    Получить погоду
  </button>
</div>

Обратите внимание, что мы пока не назначили функцию на клик по кнопке. Давайте это исправим, создав функцию fetchWeather, которая будет отвечать за весь процесс получения данных.

Пишем функцию для API-запроса с помощью fetch

Теперь самая интересная часть, взаимодействие с API. Мы будем использовать нативную браузерную функцию fetch(), которая предназначена для выполнения HTTP-запросов. Она возвращает Promise, что позволяет нам использовать современный синтаксис async/await для написания чистого и читаемого асинхронного кода.

Наша функция fetchWeather будет выполнять следующие шаги:

  1. Проверить, не пустое ли поле city.

  2. Установить состояние loading в true (чтобы показать пользователю, что идет загрузка) и сбросить возможные предыдущие ошибки.

  3. Выполнить fetch-запрос к URL API OpenWeatherMap, подставив в него город и наш API-ключ.

  4. Дождаться ответа и преобразовать его в JSON.

  5. Если ответ успешный (свойство ok равно true), обновить состояние weatherData полученными данными.

  6. Если произошла ошибка (например, город не найден), обработать ее и установить сообщение в состояние error.

  7. В любом случае, по окончании запроса установить loading обратно в false.

jsx
const fetchWeather = async () => {
  // ВАЖНО: подставьте сюда свой настоящий API-ключ
  const apiKey = 'ВАШ_API_КЛЮЧ';
  if (!city.trim()) {
    setError('Пожалуйста, введите город');
    return;
  }

  setLoading(true);
  setError('');
  setWeatherData(null); // Сбрасываем предыдущие данные

  try {
    // Формируем URL для запроса. Мы используем эндпоинт для текущей погоды.
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}&lang=ru`;
    
    // Выполняем запрос и ждем ответ
    const response = await fetch(url);
    
    // Если ответ не успешный, выбрасываем ошибку
    if (!response.ok) {
      // OpenWeatherMap возвращает 404, если город не найден
      throw new Error('Город не найден. Проверьте правильность написания.');
    }
    
    // Парсим JSON из ответа
    const data = await response.json();
    
    // Обновляем состояние с полученными данными
    setWeatherData(data);
  } catch (err) {
    // Ловим и обрабатываем любые ошибки в запросе
    setError(err.message);
  } finally {
    // Этот блок выполнится в любом случае, был успех или ошибка
    setLoading(false);
  }
};

Не забудьте вызвать эту функцию при клике на кнопку, обновив JSX:

jsx
<button onClick={fetchWeather}>Получить погоду</button>

Обрабатываем состояния: загрузка, ошибка и успешный результат

Наше приложение может находиться в нескольких состояниях: ожидание, загрузка, ошибка или отображение данных. Хороший UX предполагает, что мы визуально сообщаем пользователю о текущем состоянии. Для этого мы используем состояния loading и error. В JSX мы можем организовать условный рендеринг.

jsx
// ... внутри return компонента App, после формы поиска
<div className="weather-container">
  {loading && <p>Загружаем данные о погоде...</p>}
  {error && <p className="error">Ошибка: {error}</p>}
  {!loading && !error && weatherData && (
    <div className="weather-card">
      <h2>
        Погода в {weatherData.name}, {weatherData.sys.country}
      </h2>
      <div className="weather-main">
        <img
          src={`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`}
          alt={weatherData.weather[0].description}
        />
        <p className="temperature">
          {Math.round(weatherData.main.temp)}°C
        </p>
      </div>
      <p className="weather-description">
        {weatherData.weather[0].description}
      </p>
      <div className="weather-details">
        <p>Влажность: {weatherData.main.humidity}%</p>
        <p>Ветер: {weatherData.wind.speed} м/с</p>
        <p>Ощущается как: {Math.round(weatherData.main.feels_like)}°C</p>
      </div>
    </div>
  )}
</div>

Давайте разберем, что здесь происходит. Сначала мы проверяем, если loading равно true, то показываем сообщение о загрузке. Затем проверяем, есть ли ошибка и если есть, показываем ее. И наконец, если загрузка не идет и ошибок нет, но при этом weatherData не null (то есть данные были успешно получены), мы рендерим карточку с погодой.

В карточке мы используем данные из объекта weatherData, который нам вернул API. Структура этого объекта описана в документации OpenWeatherMap. Обратите внимание, что для отображения иконки мы используем специальный URL, в который подставляем код иконки из ответа. Это стандартная практика работы с этим API.

Добавляем немного стилей CSS

Чтобы наше приложение выглядело презентабельно, добавим базовые стили. Создадим или обновим файл App.css. Вот пример простого, но современного стиля.

css
/* App.css */
body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
  min-height: 100vh;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #333;
}

.app {
  background: white;
  padding: 2rem;
  border-radius: 16px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
  text-align: center;
  max-width: 400px;
  width: 90%;
}

h1 {
  margin-bottom: 1.5rem;
  color: #2d3436;
}

.search-box {
  margin-bottom: 2rem;
  display: flex;
  gap: 10px;
}

.search-box input {
  flex-grow: 1;
  padding: 12px 16px;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-size: 1rem;
  outline: none;
  transition: border-color 0.3s;
}

.search-box input:focus {
  border-color: #74b9ff;
}

.search-box button {
  padding: 12px 20px;
  background: #0984e3;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.3s;
}

.search-box button:hover {
  background: #0770c4;
}

.weather-card {
  animation: fadeIn 0.5s ease-in;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.weather-main {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  margin: 1.5rem 0;
}

.temperature {
  font-size: 3rem;
  font-weight: bold;
  margin: 0;
  color: #0984e3;
}

.weather-description {
  text-transform: capitalize;
  font-size: 1.2rem;
  margin-bottom: 1.5rem;
  color: #636e72;
}

.weather-details {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  background: #f8f9fa;
  padding: 1rem;
  border-radius: 8px;
}

.weather-details p {
  margin: 0;
  text-align: left;
}

.error {
  color: #d63031;
  background: #ffebee;
  padding: 1rem;
  border-radius: 8px;
  border: 1px solid #d63031;
}

Эти стили придают нашему приложению аккуратный и современный вид. Мы используем CSS Grid для деталей погоды и Flexbox для выравнивания элементов. Небольшая анимация появления карточки (fadeIn) делает интерфейс более живым.

Практические задачи для закрепления

Чтобы глубже понять материал и улучшить приложение, я предлагаю вам выполнить несколько практических задач. Начните с простых и постепенно переходите к более сложным.

  1. Базовая задача. Улучшите обработку ошибок. Сейчас мы обрабатываем только ошибку «город не найден». Но что, если проблема с интернет-соединением? Добавьте проверку в блок catch, которая будет определять тип ошибки и показывать соответствующее сообщение (например, «Проблемы с подключением к интернету»).

  2. Задача среднего уровня. Добавьте поиск по нажатию клавиши Enter. Сейчас поиск происходит только по клику на кнопку. Сделайте так, чтобы при вводе города в поле и нажатии клавиши Enter также выполнялся запрос. Для этого нужно обработать событие onKeyPress или onKeyDown на input.

    Подсказка:

    jsx
    const handleKeyPress = (e) => {
      if (e.key === 'Enter') {
        fetchWeather();
      }
    };
    // И добавьте onKeyPress={handleKeyPress} к input
  3. Продвинутая задача. Реализуйте кеширование. Чтобы не делать лишние запросы к API для одного и того же города, сохраняйте последние несколько запросов в состоянии. Перед тем как делать новый запрос, проверяйте, нет ли уже данных для этого города в кеше. Для этого можно использовать объект, где ключом будет название города, а значением данные о погоде.

  4. Креативная задача. Добавьте возможность просмотра прогноза погоды на несколько дней. Изучите в документации OpenWeatherMap эндпоинт для 5-дневного прогноза (API call «5 day / 3 hour forecast»). Создайте новый компонент, который будет отображать погоду на следующие дни, возможно, в виде горизонтального списка или табов.

Итоги и выводы

Вы только что создали свое первое полноценное React-приложение, взаимодействующее с внешним API. Это огромный шаг вперед. Давайте еще раз пройдемся по тому, что мы с вами сделали: мы спланировали приложение, научились работать с API-ключом, создали структуру состояния с помощью useState, написали асинхронную функцию для получения данных, обработали состояния загрузки и ошибок и красиво отобразили данные в интерфейсе.

Этот проект является отличным фундаментом. Паттерны, которые вы здесь применили (управление состоянием, асинхронные запросы, условный рендеринг), являются универсальными и будут использоваться в 90% React-приложений, которые вы создадите в будущем.

Если вы хотите продолжить системное изучение React и разобраться с такими темами, как маршрутизация с React Router, то приглашаю вас на полный курс с уроками по React для начинающих. Там вас ждут структурированные материалы, сложные проекты и поддержка сообщества.

Удачи в изучении React!

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий