Сегодня у нас настал тот самый момент, когда теория встречается с практикой. Мы прошли долгий путь, изучили 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. Во-вторых, это значение из поля ввода (название города). И в-третьих, нам понадобится состояние для отслеживания загрузки и возможных ошибок.
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 обновляет состояние.
// ... внутри 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 будет выполнять следующие шаги:
-
Проверить, не пустое ли поле
city. -
Установить состояние
loadingвtrue(чтобы показать пользователю, что идет загрузка) и сбросить возможные предыдущие ошибки. -
Выполнить
fetch-запрос к URL API OpenWeatherMap, подставив в него город и наш API-ключ. -
Дождаться ответа и преобразовать его в JSON.
-
Если ответ успешный (свойство
okравноtrue), обновить состояниеweatherDataполученными данными. -
Если произошла ошибка (например, город не найден), обработать ее и установить сообщение в состояние
error. -
В любом случае, по окончании запроса установить
loadingобратно вfalse.
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:
<button onClick={fetchWeather}>Получить погоду</button>
Обрабатываем состояния: загрузка, ошибка и успешный результат
Наше приложение может находиться в нескольких состояниях: ожидание, загрузка, ошибка или отображение данных. Хороший UX предполагает, что мы визуально сообщаем пользователю о текущем состоянии. Для этого мы используем состояния loading и error. В 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. Вот пример простого, но современного стиля.
/* 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) делает интерфейс более живым.
Практические задачи для закрепления
Чтобы глубже понять материал и улучшить приложение, я предлагаю вам выполнить несколько практических задач. Начните с простых и постепенно переходите к более сложным.
-
Базовая задача. Улучшите обработку ошибок. Сейчас мы обрабатываем только ошибку «город не найден». Но что, если проблема с интернет-соединением? Добавьте проверку в блок
catch, которая будет определять тип ошибки и показывать соответствующее сообщение (например, «Проблемы с подключением к интернету»). -
Задача среднего уровня. Добавьте поиск по нажатию клавиши Enter. Сейчас поиск происходит только по клику на кнопку. Сделайте так, чтобы при вводе города в поле и нажатии клавиши Enter также выполнялся запрос. Для этого нужно обработать событие
onKeyPressилиonKeyDownна input.Подсказка:
const handleKeyPress = (e) => { if (e.key === 'Enter') { fetchWeather(); } }; // И добавьте onKeyPress={handleKeyPress} к input
-
Продвинутая задача. Реализуйте кеширование. Чтобы не делать лишние запросы к API для одного и того же города, сохраняйте последние несколько запросов в состоянии. Перед тем как делать новый запрос, проверяйте, нет ли уже данных для этого города в кеше. Для этого можно использовать объект, где ключом будет название города, а значением данные о погоде.
-
Креативная задача. Добавьте возможность просмотра прогноза погоды на несколько дней. Изучите в документации OpenWeatherMap эндпоинт для 5-дневного прогноза (API call «5 day / 3 hour forecast»). Создайте новый компонент, который будет отображать погоду на следующие дни, возможно, в виде горизонтального списка или табов.
Итоги и выводы
Вы только что создали свое первое полноценное React-приложение, взаимодействующее с внешним API. Это огромный шаг вперед. Давайте еще раз пройдемся по тому, что мы с вами сделали: мы спланировали приложение, научились работать с API-ключом, создали структуру состояния с помощью useState, написали асинхронную функцию для получения данных, обработали состояния загрузки и ошибок и красиво отобразили данные в интерфейсе.
Этот проект является отличным фундаментом. Паттерны, которые вы здесь применили (управление состоянием, асинхронные запросы, условный рендеринг), являются универсальными и будут использоваться в 90% React-приложений, которые вы создадите в будущем.
Если вы хотите продолжить системное изучение React и разобраться с такими темами, как маршрутизация с React Router, то приглашаю вас на полный курс с уроками по React для начинающих. Там вас ждут структурированные материалы, сложные проекты и поддержка сообщества.
Удачи в изучении React!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


