كيفية بناء تطبيق طقس باستخدام React وReact Hooks خطوة بخطوة
مقدمة: لماذا يُعد بناء تطبيق طقس باستخدام React مشروعاً مثالياً؟
يُعد تطبيق الطقس من المشاريع العملية الممتازة لفهم أساسيات تطوير الواجهات الحديثة، لأنه يجمع بين أكثر من مفهوم مهم في وقت واحد، مثل إدارة الحالة، استدعاء البيانات من واجهات API خارجية، التعامل مع الموقع الجغرافي للمستخدم، وعرض المعلومات بأسلوب واضح وجذاب. ومع استخدام React وميزة React Hooks، يصبح بناء هذا النوع من التطبيقات أكثر مرونة وتنظيماً.
في هذا الدليل، سنتعلم كيفية إنشاء تطبيق طقس يعرض حالة الطقس الحالية اعتماداً على موقع المستخدم، مع استخدام مكتبات مساعدة لتحسين التصميم وتنسيق الوقت والتاريخ، مع الحفاظ على بنية نظيفة وقابلة للتطوير.

تجهيز البيئة: تثبيت Node.js وnpm
قبل البدء في إنشاء المشروع، نحتاج إلى بيئة تشغيل مناسبة لتطبيقات JavaScript. هنا يأتي دور Node.js، وهو بيئة تشغيل تتيح تنفيذ شيفرات JavaScript خارج المتصفح، كما يُعد أساساً لعدد كبير من أدوات التطوير الحديثة.
أما npm فهو مدير الحزم المدمج مع Node.js، ويُستخدم لتثبيت المكتبات والأدوات التي يحتاجها المشروع.
خطوات التثبيت والتحقق
- توجه إلى الموقع الرسمي:
https://nodejs.org/en/ - نزّل النسخة المناسبة لنظامك.
- بعد اكتمال التثبيت، افتح الطرفية
Terminalأو موجه الأوامرCommand Prompt. - نفّذ الأمر التالي للتحقق من نجاح التثبيت:
node -v
إذا ظهر رقم إصدار، فهذا يعني أن Node.js يعمل بشكل صحيح.
إنشاء مشروع React جديد
لإنشاء التطبيق بسرعة، يمكن استخدام أداة create-react-app التي توفر هيكلاً جاهزاً لبدء التطوير.
npx create-react-app my-weather-app
بعد اكتمال تثبيت الحزم، ادخل إلى مجلد المشروع ثم شغّل التطبيق:
cd my-weather-app
npm start
عندها ستظهر لك الواجهة الافتراضية الخاصة بـReact.


تنظيف القالب الافتراضي
لا نحتاج إلى جميع العناصر المبدئية الموجودة داخل ملف App.js. لذلك يمكن حذف المحتوى داخل وسم div الرئيسي، وإزالة استيراد الشعار الافتراضي. بعد ذلك ستظهر شاشة فارغة، وهذا أمر طبيعي لأنه يمثل نقطة انطلاق نظيفة للمشروع.

تثبيت المكتبات المطلوبة للمشروع
لجعل التطبيق أكثر أناقة وسهولة في العرض، سنستخدم بعض الحزم الخارجية. أهم هذه الحزم:
semantic-ui-reactلبناء عناصر واجهة جاهزة.semantic-ui-cssلتوفير أنماط المكتبة.momentلتنسيق التاريخ والوقت بسهولة.
تثبيت مكتبة Semantic UI
npm install semantic-ui-react semantic-ui-css
بعد التثبيت، افتح ملف index.js وأضف السطر التالي:
import 'semantic-ui-css/semantic.min.css'
تثبيت مكتبة moment
npm install moment --save
يمكنك مراجعة ملف package.json لمتابعة جميع الحزم التي تم تثبيتها داخل المشروع.

الحصول على بيانات الطقس عبر OpenWeatherMap API
لكي يعمل التطبيق فعلياً، نحتاج إلى مصدر خارجي يزوّدنا ببيانات الطقس. من أشهر هذه الخدمات منصة OpenWeatherMap، وهي توفر واجهات API متعددة تشمل الطقس الحالي والتوقعات المستقبلية وغيرها.
خطوات إعداد الحساب والمفتاح البرمجي
- أنشئ حساباً عبر الرابط:
https://home.openweathermap.org/users/sign_up - بعد تسجيل الدخول، انتقل إلى قسم
API. - ستجد عدة نقاط وصول
Endpointsمثل بيانات الطقس الحالية والتوقعات الساعية واليومية. - من قائمة الحساب، ادخل إلى قسم
My API keys. - أنشئ مفتاحاً جديداً إذا لم يكن لديك واحد بالفعل.

إنشاء ملف المتغيرات البيئية .env
داخل المجلد الرئيسي للمشروع، أنشئ ملفاً باسم .env ثم أضف القيم التالية:
REACT_APP_API_URL='https://api.openweathermap.org/data/2.5'
REACT_APP_API_KEY=YOUR_API_KEY_HERE
REACT_APP_ICON_URL='https://openweathermap.org/img/w'
استبدل القيمة YOUR_API_KEY_HERE بمفتاح API الحقيقي الخاص بك. استخدام ملف .env يساعد على تنظيم الإعدادات وفصلها عن منطق التطبيق.
فهم React Hooks واستخدامها في المشروع
توفر React Hooks وسيلة حديثة لإدارة الحالة والتعامل مع دورة حياة المكونات الوظيفية دون الحاجة إلى المكونات الصنفية Class Components. في هذا المشروع سنعتمد على:
useStateلتخزين القيم المتغيرة مثل خط العرض وخط الطول وبيانات الطقس.useEffectلتنفيذ الأوامر عند تحميل الصفحة أو تحديث القيم المرتبطة بها.
استيراد الأدوات الأساسية
import React, { useEffect, useState } from "react";
إنشاء الحالات الأساسية
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
هنا نُعرّف حالتين لتخزين خط العرض latitude وخط الطول longitude الخاصين بموقع المستخدم.
استخدام useEffect للحصول على الموقع الجغرافي
useEffect(() => {
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
console.log("Latitude is:", lat);
console.log("Longitude is:", long);
}, [lat, long]);
تعتمد هذه الخطوة على navigator.geolocation لجلب إحداثيات المستخدم بعد الحصول على إذنه. ثم يتم تخزين هذه القيم عبر الدالتين setLat وsetLong.
الشكل الحالي لملف App.js
import './App.css';
import React, { useEffect, useState } from "react";
export default function App() {
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
useEffect(() => {
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
console.log("Latitude is:", lat);
console.log("Longitude is:", long);
}, [lat, long]);
return (
<div className="App"></div>
);
}
بعد تنفيذ ذلك، ستتمكن من رؤية الإحداثيات في وحدة التحكم Console.
Latitude is: 25.5922166
Longitude is: 85.12761069999999
جلب بيانات الطقس وفقاً للموقع الحالي
بعد الحصول على الموقع الجغرافي، ننتقل إلى الخطوة الأهم: جلب حالة الطقس الحالية باستخدام fetch وربط الإحداثيات بواجهة OpenWeatherMap API.
إنشاء حالة لتخزين بيانات الطقس
const [data, setData] = useState([]);
تنفيذ طلب fetch
await fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`)
.then(res => res.json())
.then(result => {
setData(result);
console.log(result);
});
في هذا السطر، نستخدم:
process.env.REACT_APP_API_URLللوصول إلى عنوان الواجهة البرمجية.process.env.REACT_APP_API_KEYلاستخدام المفتاح المخزن في ملف.env.latوlongلتمرير إحداثيات المستخدم.units=metricللحصول على درجات الحرارة بالمقياس المئوي.

النسخة المحدثة من ملف App.js
import './App.css';
import React, { useEffect, useState } from "react";
import Weather from './components/weather';
export default function App() {
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
await fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`)
.then(res => res.json())
.then(result => {
setData(result);
console.log(result);
});
};
fetchData();
}, [lat, long]);
return (
<div className="App"></div>
);
}
إنشاء مكوّن عرض الطقس Weather Component
حتى نحافظ على تنظيم المشروع، من الأفضل فصل واجهة عرض بيانات الطقس في مكوّن مستقل. داخل مجلد src، أنشئ مجلداً باسم components ثم ملفاً باسم weather.js.
استدعاء المكوّن داخل App.js
import './App.css';
import React, { useEffect, useState } from "react";
import Weather from './components/weather';
export default function App() {
const [lat, setLat] = useState([]);
const [long, setLong] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
navigator.geolocation.getCurrentPosition(function (position) {
setLat(position.coords.latitude);
setLong(position.coords.longitude);
});
await fetch(`${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`)
.then(res => res.json())
.then(result => {
setData(result);
console.log(result);
});
};
fetchData();
}, [lat, long]);
return (
<div className="App">
{(typeof data.main !== 'undefined') ? (
<Weather weatherData={data} />
) : (
<div></div>
)}
</div>
);
}
التحقق من أن data.main ليس undefined خطوة ضرورية، لأن جلب البيانات يتم بشكل غير متزامن async. ولو حاولت الواجهة الوصول إلى خصائص غير موجودة قبل اكتمال الطلب، فسيظهر خطأ أثناء التصيير.

تمرير البيانات عبر Props بين المكونات
في React، تُستخدم Props لنقل البيانات من المكوّن الأب إلى المكوّن الابن. في مشروعنا:
- المكوّن الأب هو
App.js. - المكوّن الابن هو
weather.js.
تمرير البيانات يتم بهذا الشكل:
<Weather weatherData={data} />
واستقبالها داخل الملف الآخر يتم على النحو التالي:
import React from 'react';
import './styles.css';
import { Card } from 'semantic-ui-react';
const CardExampleCard = ({ weatherData }) => (
<Card>
<Card.Content>
<Card.Header className="header">{weatherData.name}</Card.Header>
</Card.Content>
</Card>
);
export default CardExampleCard;
في هذه المرحلة، سيظهر اسم المدينة بناءً على موقع المستخدم.

عرض تفاصيل الطقس الأساسية داخل الواجهة
بعد التأكد من وصول البيانات بنجاح، يمكن توسيع المكوّن ليعرض معلومات إضافية مثل درجة الحرارة، وقت الشروق والغروب، والوصف العام للحالة الجوية.
import React from 'react';
import './styles.css';
import { Card } from 'semantic-ui-react';
const CardExampleCard = ({ weatherData }) => (
<Card>
<Card.Content>
<Card.Header className="header">City Name: {weatherData.name}</Card.Header>
<p>Temprature: {weatherData.main.temp}</p>
<p>Sunrise: {weatherData.sys.sunrise}</p>
<p>Sunset: {weatherData.sys.sunset}</p>
<p>Description: {weatherData.weather[0].description}</p>
</Card.Content>
</Card>
);
export default CardExampleCard;

كما يمكنك إضافة حقول أخرى مثل:
- الرطوبة
Humidity - سرعة الرياح
Wind Speed - مدى الرؤية
Visibility - الضغط الجوي
Pressure
تنسيق البيانات لتحسين القراءة وتجربة الاستخدام
عرض البيانات الخام كما تأتي من API ليس الأفضل دائماً، لذلك من المهم تحويلها إلى صيغة مفهومة للمستخدم.
إظهار الحرارة بالمقياس المئوي
يمكنك إضافة الرمز °C إلى درجة الحرارة حتى تصبح أكثر وضوحاً.
تحويل وقت الشروق والغروب إلى توقيت محلي
import React from 'react';
import './styles.css';
import { Card } from 'semantic-ui-react';
const CardExampleCard = ({ weatherData }) => (
<Card>
<Card.Content>
<Card.Header className="header">City Name: {weatherData.name}</Card.Header>
<p>Temprature: {weatherData.main.temp} ° C</p>
<p>Sunrise: {new Date(weatherData.sys.sunrise * 1000).toLocaleTimeString('en-IN')}</p>
<p>Sunset: {new Date(weatherData.sys.sunset * 1000).toLocaleTimeString('en-IN')}</p>
<p>Description: {weatherData.weather[0].main}</p>
<p>Humidity: {weatherData.main.humidity} %</p>
</Card.Content>
</Card>
);
export default CardExampleCard;
تُعاد قيم الشروق والغروب من API بصيغة Unix Timestamp، لذلك نضربها في 1000 ثم نحوّلها إلى وقت محلي مفهوم.
إضافة اليوم والتاريخ باستخدام moment
import moment from 'moment';
<p>Day: {moment().format('dddd')}</p>
<p>Date: {moment().format('LL')}</p>
تسهل مكتبة moment عرض التاريخ واليوم الحاليين بصيغة جاهزة وسلسة.
الشكل المحدث لملف weather.js
import React from 'react';
import './styles.css';
import { Card } from 'semantic-ui-react';
import moment from 'moment';
const CardExampleCard = ({ weatherData }) => (
<Card>
<Card.Content>
<Card.Header className="header">City Name: {weatherData.name}</Card.Header>
<p>Temprature: {weatherData.main.temp} ° C</p>
<p>Sunrise: {new Date(weatherData.sys.sunrise * 1000).toLocaleTimeString('en-IN')}</p>
<p>Sunset: {new Date(weatherData.sys.sunset * 1000).toLocaleTimeString('en-IN')}</p>
<p>Description: {weatherData.weather[0].main}</p>
<p>Humidity: {weatherData.main.humidity} %</p>
<p>Day: {moment().format('dddd')}</p>
<p>Date: {moment().format('LL')}</p>
</Card.Content>
</Card>
);
export default CardExampleCard;

تحسين تصميم الواجهة باستخدام CSS
بعد اكتمال منطق التطبيق، تأتي مرحلة التحسين البصري. الهدف هنا ليس الجمال فقط، بل أيضاً تسهيل قراءة البيانات وجعل الواجهة أكثر احترافية.
تحديث هيكل المكوّن
import React from 'react';
import './styles.css';
import moment from 'moment';
const CardExampleCard = ({ weatherData }) => (
<div className="main">
<p className="header">{weatherData.name}</p>
<div>
<p className="day">Day: {moment().format('dddd')}</p>
</div>
<div>
<p className="temp">Temprature: {weatherData.main.temp} ° C</p>
</div>
</div>
);
export default CardExampleCard;
تنسيق ملف styles.css
@import url('https://fonts.googleapis.com/css2?family=Recursive&display=swap');
.main {
width: 700px;
border-radius: 15px;
background-color: #01579b;
}
.header {
background-color: #424242;
color: whitesmoke;
padding: 10px;
font-size: 28px;
border-radius: 15px;
font-family: 'Recursive', sans-serif;
}
.day {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 24px;
font-weight: 600;
}
.temp {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 18px;
}

ترتيب البيانات باستخدام Flexbox
لتوزيع البيانات أفقياً بشكل مرتب، يمكن استخدام flexbox كما يلي:
<div className="flex">
<p className="day">Day: {moment().format('dddd')}</p>
</div>
<div className="flex">
<p className="temp">Temprature: {weatherData.main.temp} ° C</p>
</div>
ثم أضف النمط التالي:
.flex {
display: flex;
justify-content: space-between;
}
نسخة أكثر اكتمالاً من مكوّن الطقس
import React from 'react';
import './styles.css';
import moment from 'moment';
const WeatherCard = ({ weatherData }) => (
<div className="main">
<p className="header">{weatherData.name}</p>
<div className="flex">
<p className="day">{moment().format('dddd')}, <span>{moment().format('LL')}</span></p>
<p className="description">{weatherData.weather[0].main}</p>
</div>
<div className="flex">
<p className="temp">Temprature: {weatherData.main.temp} ° C</p>
<p className="temp">Humidity: {weatherData.main.humidity} %</p>
</div>
<div className="flex">
<p className="sunrise-sunset">Sunrise: {new Date(weatherData.sys.sunrise * 1000).toLocaleTimeString('en-IN')}</p>
<p className="sunrise-sunset">Sunset: {new Date(weatherData.sys.sunset * 1000).toLocaleTimeString('en-IN')}</p>
</div>
</div>
);
export default WeatherCard;
@import url('https://fonts.googleapis.com/css2?family=Recursive&display=swap');
.main {
width: 700px;
border-radius: 20px;
background-color: #01579b;
}
.top {
height: 60px;
background-color: #424242;
color: whitesmoke;
padding: 10px;
border-radius: 20px 20px 0 0;
font-family: 'Recursive', sans-serif;
display: flex;
justify-content: space-between;
}
.header {
background-color: #424242;
color: whitesmoke;
margin: 10px 0 0 10px;
font-size: 25px;
border-radius: 20px 20px 0 0;
font-family: 'Recursive', sans-serif;
}
.day {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 24px;
font-weight: 600;
}
.temp {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 18px;
}
.flex {
display: flex;
justify-content: space-between;
}
.sunrise-sunset {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 16px;
}
.description {
padding: 15px;
color: whitesmoke;
font-family: 'Recursive', sans-serif;
font-size: 24px;
font-weight: 600;
}

إضافة زر تحديث لإعادة تحميل البيانات
وجود زر تحديث مفيد حين يرغب المستخدم في إعادة جلب البيانات فوراً، خصوصاً إذا تغيّر الموقع أو أراد تحديث حالة الطقس الحالية.
import React from 'react';
import './styles.css';
import moment from 'moment';
import { Button } from 'semantic-ui-react';
const refresh = () => {
window.location.reload();
};
const WeatherCard = ({ weatherData }) => (
<div className="main">
<div className="top">
<p className="header">{weatherData.name}</p>
<Button
className="button"
inverted
color='blue'
circular
icon='refresh'
onClick={refresh}
/>
</div>
<div className="flex">
<p className="day">{moment().format('dddd')}, <span>{moment().format('LL')}</span></p>
<p className="description">{weatherData.weather[0].main}</p>
</div>
<div className="flex">
<p className="temp">Temprature: {weatherData.main.temp} ° C</p>
<p className="temp">Humidity: {weatherData.main.humidity} %</p>
</div>
<div className="flex">
<p className="sunrise-sunset">Sunrise: {new Date(weatherData.sys.sunrise * 1000).toLocaleTimeString('en-IN')}</p>
<p className="sunrise-sunset">Sunset: {new Date(weatherData.sys.sunset * 1000).toLocaleTimeString('en-IN')}</p>
</div>
</div>
);
export default WeatherCard;
.button {
width: 35px;
height: 35px;
}

إضافة شاشة تحميل Loader أثناء جلب البيانات
من أفضل ممارسات تجربة المستخدم أن يرى الزائر مؤشراً بصرياً أثناء انتظار تحميل البيانات، بدلاً من شاشة فارغة.
import { Dimmer, Loader } from 'semantic-ui-react';
<div className="App">
{(typeof data.main !== 'undefined') ? (
<Weather weatherData={data} />
) : (
<div>
<Dimmer active>
<Loader>Loading..</Loader>
</Dimmer>
</div>
)}
</div>
هذه الإضافة البسيطة تمنح التطبيق مظهراً أكثر احترافية وتقلل من ارتباك المستخدم أثناء انتظار البيانات.
ملخص المفاهيم التي يطبقها هذا المشروع
إدارة الحالة باستخدام State
تم استخدام الحالة لتخزين معلومات متغيرة مثل الإحداثيات وبيانات الطقس. هذه القيم تختلف من مستخدم لآخر حسب موقعه الحالي، لذلك تُعد State جزءاً أساسياً من التطبيق.
نقل البيانات عبر Props
تم جلب البيانات في الملف الرئيسي App.js ثم تمريرها إلى المكوّن الفرعي weather.js باستخدام Props. وهذا يوضح كيف يحافظ React على تدفق بيانات منظم من الأعلى إلى الأسفل.
استخدام React Hooks بدل المكونات الصنفية
بدلاً من الاعتماد على أساليب دورة الحياة التقليدية في Class Components، تم استخدام useState وuseEffect لبناء التطبيق بشكل أبسط وأكثر حداثة.
الاعتماد على مكتبات واجهة جاهزة
ساعدت مكتبة Semantic UI في تسريع بناء عناصر الواجهة، بينما وفرت مكتبة moment طريقة عملية لتنسيق اليوم والتاريخ.
أفكار لتطوير التطبيق مستقبلاً
بعد الانتهاء من النسخة الأساسية، يمكنك توسيع التطبيق وإضافة مزايا أكثر تقدماً، مثل:
- عرض توقعات الطقس لخمسة أيام.
- إضافة أيقونات الطقس الديناميكية.
- إتاحة البحث عن مدينة بدلاً من الاعتماد على الموقع فقط.
- إضافة دعم الوضع الليلي
Dark Mode. - تحسين الاستجابة على الهواتف المحمولة.
- إضافة معالجة أفضل للأخطاء عند رفض إذن الموقع أو تعطل
API.
الخلاصة التقنية
بناء تطبيق طقس باستخدام React وReact Hooks يُعد تدريباً عملياً ممتازاً على تطوير واجهات حديثة تتفاعل مع بيانات خارجية في الزمن الحقيقي. هذا المشروع لا يشرح فقط كيفية جلب البيانات وعرضها، بل يرسخ أيضاً مفاهيم مهمة مثل فصل المكونات، إدارة الحالة، التعامل مع العمليات غير المتزامنة، وتحسين تجربة المستخدم بصرياً ووظيفياً. وإذا تم تنفيذ الخطوات بعناية مع تحسينات إضافية في الأداء ومعالجة الأخطاء، فيمكن تحويل هذا المشروع البسيط إلى تطبيق فعلي متكامل قابل للنشر والاستخدام.