ما هو React.js؟
React.js هو مكتبة JavaScript مفتوحة المصدر طورتها شركة Facebook (Meta) عام 2013 لبناء واجهات مستخدم تفاعلية وسريعة. يعتمد React على مفهوم المكونات (Components) القابلة لإعادة الاستخدام، حيث يتم تقسيم واجهة المستخدم إلى أجزاء صغيرة مستقلة يمكن تطويرها واختبارها بشكل منفصل ثم تجميعها لبناء تطبيقات معقدة.
يستخدم React أكثر من 10 ملايين مطور حول العالم، وتعتمد عليه شركات كبرى مثل Facebook و Instagram و Netflix و Airbnb و Uber. يتميز React بـ Virtual DOM الذي يحسّن الأداء بشكل كبير عن طريق تقليل عمليات التحديث المباشرة على DOM الحقيقي، وبصيغة JSX التي تجمع بين JavaScript و HTML بشكل أنيق.
إعداد بيئة التطوير
إنشاء مشروع React جديد
# الطريقة الحديثة باستخدام Vite (أسرع بكثير)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
# هيكل المشروع
# my-app/
# ├── public/
# │ └── vite.svg
# ├── src/
# │ ├── assets/
# │ ├── components/
# │ │ ├── Header.jsx
# │ │ ├── Footer.jsx
# │ │ └── Card.jsx
# │ ├── pages/
# │ │ ├── Home.jsx
# │ │ └── About.jsx
# │ ├── hooks/
# │ │ └── useLocalStorage.js
# │ ├── App.jsx
# │ ├── App.css
# │ └── main.jsx
# ├── package.json
# └── vite.config.js
# تثبيت أدوات إضافية شائعة
npm install react-router-dom axios
npm install -D tailwindcss postcss autoprefixer
أساسيات JSX
JSX هو امتداد لصيغة JavaScript يسمح بكتابة HTML داخل كود JavaScript. يبدو مثل HTML لكنه في الحقيقة يتحول إلى استدعاءات JavaScript عند التجميع:
// JSX الأساسي
function Welcome() {
const name = "أحمد";
const isLoggedIn = true;
const items = ["React", "Vue", "Angular"];
return (
<div className="container">
{/* التعليقات في JSX */}
<h1>مرحباً {name}!</h1>
{/* عرض شرطي */}
{isLoggedIn ? (
<p>مرحباً بعودتك</p>
) : (
<p>سجّل الدخول</p>
)}
{/* عرض قائمة */}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* عرض شرطي مختصر */}
{isLoggedIn && <button>تسجيل الخروج</button>}
</div>
);
}
export default Welcome;
المكونات وأنواعها
المكونات الوظيفية (Function Components)
// مكون بسيط
function Greeting({ name, role }) {
return (
<div className="greeting">
<h2>مرحباً {name}</h2>
<p>الدور: {role}</p>
</div>
);
}
// مكون بطاقة قابل لإعادة الاستخدام
function Card({ title, description, image, children }) {
return (
<div className="card">
{image && <img src={image} alt={title} />}
<div className="card-body">
<h3>{title}</h3>
<p>{description}</p>
{children}
</div>
</div>
);
}
// الاستخدام
function App() {
return (
<div>
<Greeting name="سارة" role="مطورة" />
<Card
title="تعلم React"
description="دورة شاملة"
image="/react.png"
>
<button>ابدأ الآن</button>
</Card>
</div>
);
}
إدارة الحالة مع useState
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>العداد: {count}</h2>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>إعادة تعيين</button>
</div>
);
}
// مثال متقدم: نموذج تسجيل
function RegistrationForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// مسح الخطأ عند الكتابة
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: "" }));
}
};
const validate = () => {
const newErrors = {};
if (!formData.name) newErrors.name = "الاسم مطلوب";
if (!formData.email.includes("@")) newErrors.email = "بريد غير صالح";
if (formData.password.length < 8) newErrors.password = "كلمة المرور قصيرة";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
setIsSubmitting(true);
// إرسال البيانات...
setIsSubmitting(false);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={formData.name}
onChange={handleChange} placeholder="الاسم" />
{errors.name && <span className="error">{errors.name}</span>}
<input name="email" type="email" value={formData.email}
onChange={handleChange} placeholder="البريد" />
{errors.email && <span className="error">{errors.email}</span>}
<input name="password" type="password" value={formData.password}
onChange={handleChange} placeholder="كلمة المرور" />
{errors.password && <span className="error">{errors.password}</span>}
<button disabled={isSubmitting}>
{isSubmitting ? "جاري الإرسال..." : "تسجيل"}
</button>
</form>
);
}
التأثيرات الجانبية مع useEffect
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch("/api/users/" + userId);
if (!response.ok) throw new Error("فشل في الجلب");
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // يعمل عند تغيّر userId
if (loading) return <div className="loader">جاري التحميل...</div>;
if (error) return <div className="error">خطأ: {error}</div>;
if (!user) return null;
return (
<div className="profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// useEffect مع تنظيف (Cleanup)
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// تنظيف عند إزالة المكون
return () => clearInterval(interval);
}, []);
return <p>الوقت: {seconds} ثانية</p>;
}
Hooks مخصصة (Custom Hooks)
// Hook مخصص لجلب البيانات
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const res = await fetch(url, { signal: controller.signal });
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Hook للتخزين المحلي
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// الاستخدام
function ProductsList() {
const { data, loading, error } = useFetch("/api/products");
const [theme, setTheme] = useLocalStorage("theme", "light");
if (loading) return <p>جاري التحميل...</p>;
return (
<div className={theme}>
{data?.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
التنقل مع React Router
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from "react-router-dom";
// تعريف المسارات
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">الرئيسية</Link>
<Link to="/products">المنتجات</Link>
<Link to="/about">عن الموقع</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// صفحة تفاصيل المنتج مع معامل ديناميكي
function ProductDetail() {
const { id } = useParams();
const navigate = useNavigate();
const { data: product, loading } = useFetch("/api/products/" + id);
if (loading) return <p>جاري التحميل...</p>;
return (
<div>
<button onClick={() => navigate(-1)}>رجوع</button>
<h1>{product?.name}</h1>
<p>{product?.description}</p>
</div>
);
}
مشروع تطبيقي: تطبيق قائمة مهام كامل
import { useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState("");
const [filter, setFilter] = useState("all");
const addTodo = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: input.trim(),
completed: false
}]);
setInput("");
};
const toggleTodo = (id) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
const filteredTodos = todos.filter(t => {
if (filter === "active") return !t.completed;
if (filter === "completed") return t.completed;
return true;
});
const remaining = todos.filter(t => !t.completed).length;
return (
<div className="todo-app">
<h1>قائمة المهام</h1>
<form onSubmit={addTodo}>
<input value={input} onChange={e => setInput(e.target.value)}
placeholder="أضف مهمة جديدة..." />
<button type="submit">إضافة</button>
</form>
<div className="filters">
{["all", "active", "completed"].map(f => (
<button key={f} onClick={() => setFilter(f)}
className={filter === f ? "active" : ""}>
{f === "all" ? "الكل" : f === "active" ? "نشطة" : "مكتملة"}
</button>
))}
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? "done" : ""}>
<input type="checkbox" checked={todo.completed}
onChange={() => toggleTodo(todo.id)} />
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>حذف</button>
</li>
))}
</ul>
<p>المتبقي: {remaining} مهمة</p>
</div>
);
}
Context API لإدارة الحالة المشتركة
عندما تحتاج لمشاركة بيانات بين عدة مكونات دون تمريرها عبر Props في كل مستوى من شجرة المكونات (وهو ما يسمى Props Drilling)، توفر Context API حلاً أنيقاً. Context يسمح لك بإنشاء حالة عامة يمكن لأي مكون في شجرة المكونات الوصول إليها مباشرة بدون تمريرها يدوياً عبر المكونات الوسيطة.
من أشهر حالات استخدام Context: تبديل الوضع الداكن والفاتح، إدارة حالة المصادقة والمستخدم الحالي، تعدد اللغات والترجمة، وإعدادات التطبيق العامة مثل العملة والمنطقة الزمنية.
import { createContext, useContext, useState } from "react";
// إنشاء Context
const ThemeContext = createContext();
// مزود Context
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Hook مخصص للوصول السهل
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be inside ThemeProvider");
return context;
}
// الاستخدام في أي مكون
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>
{theme === "light" ? "الوضع الداكن" : "الوضع الفاتح"}
</button>
</header>
);
}
تحسين الأداء في React
مع نمو التطبيق، يصبح تحسين الأداء أمراً ضرورياً لضمان تجربة مستخدم سلسة. يوفر React عدة أدوات وتقنيات لتحسين الأداء ومنع إعادة العرض غير الضرورية التي تبطئ التطبيق:
- React.memo: يلف المكون ويمنع إعادة عرضه إذا لم تتغير الـ Props الممررة إليه. مفيد جداً للمكونات التي تستقبل بيانات ثابتة نسبياً مثل القوائم الجانبية وعناصر التنقل
- useMemo: يحفظ نتيجة عملية حسابية مكلفة ولا يعيد حسابها إلا عند تغيّر المدخلات المحددة. مفيد لتصفية وترتيب القوائم الكبيرة أو حسابات الإحصائيات
- useCallback: يحفظ مرجع الدالة ويمنع إعادة إنشائها في كل عرض. ضروري عند تمرير دوال كـ Props لمكونات ملفوفة بـ React.memo
- التحميل الكسول (Lazy Loading): يقسّم الكود إلى حزم صغيرة تُحمّل عند الحاجة بدلاً من تحميل التطبيق بالكامل. يستخدم React.lazy و Suspense لتحميل المكونات عند الطلب
- المفاتيح الصحيحة (Keys): استخدام معرّفات فريدة وثابتة كمفاتيح في القوائم بدلاً من الفهرس (index) يساعد React على تحديد العناصر المتغيرة بدقة وتجنب إعادة عرض القائمة كاملة
import { memo, useMemo, useCallback, lazy, Suspense } from "react";
// React.memo لمنع إعادة العرض
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
console.log("عرض بطاقة: " + product.name);
return (
<div className="card">
<h3>{product.name}</h3>
<p>{product.price} درهم</p>
<button onClick={() => onAddToCart(product.id)}>أضف للسلة</button>
</div>
);
});
// useMemo و useCallback
function ProductList({ products }) {
const [search, setSearch] = useState("");
const [sortBy, setSortBy] = useState("name");
// useMemo: حفظ نتيجة التصفية
const filteredProducts = useMemo(() => {
return products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1);
}, [products, search, sortBy]);
// useCallback: حفظ مرجع الدالة
const handleAddToCart = useCallback((id) => {
console.log("إضافة المنتج " + id);
}, []);
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filteredProducts.map(p => (
<ProductCard key={p.id} product={p} onAddToCart={handleAddToCart} />
))}
</div>
);
}
// التحميل الكسول
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
function App() {
return (
<Suspense fallback={<div>جاري التحميل...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
التنسيق في React
هناك عدة طرق لتنسيق مكونات React، ولكل طريقة مزاياها وعيوبها. الاختيار يعتمد على حجم المشروع ومتطلبات الفريق:
- CSS Modules: ملفات CSS مع نطاق محلي تلقائي تمنع تعارض أسماء الفئات بين المكونات. يمكن استيرادها كـ objects في JavaScript واستخدام أسمائها كخصائص
- Tailwind CSS: إطار عمل CSS المنفعي (Utility-first) الأكثر شعبية حالياً. يوفر فئات جاهزة لكل خاصية CSS تقريباً مما يسرّع التطوير بشكل كبير ويسهّل بناء تصميمات متجاوبة
- Styled Components: مكتبة CSS-in-JS تسمح بكتابة CSS داخل مكونات JavaScript مباشرة. توفر نطاقاً محلياً تلقائياً ودعماً للوضع الديناميكي والمتغيرات
- CSS العادي: الطريقة التقليدية باستخدام ملفات CSS منفصلة. بسيطة ومألوفة لكن تحتاج لتنظيم جيد لتجنب تعارض الأسماء في المشاريع الكبيرة
اختبار مكونات React
الاختبار التلقائي يضمن جودة الكود ويمنع الأخطاء عند إجراء تغييرات مستقبلية. تستخدم React Testing Library مع Jest أو Vitest لاختبار المكونات بطريقة تحاكي تفاعل المستخدم الحقيقي بدلاً من اختبار تفاصيل التنفيذ الداخلية:
import { render, screen, fireEvent } from "@testing-library/react";
import TodoApp from "./TodoApp";
describe("تطبيق المهام", () => {
test("إضافة مهمة جديدة", () => {
render(<TodoApp />);
const input = screen.getByPlaceholderText("أضف مهمة جديدة...");
const addBtn = screen.getByText("إضافة");
fireEvent.change(input, { target: { value: "تعلم React" } });
fireEvent.click(addBtn);
expect(screen.getByText("تعلم React")).toBeInTheDocument();
expect(input.value).toBe("");
});
test("حذف مهمة", () => {
render(<TodoApp />);
// إضافة مهمة أولاً
fireEvent.change(screen.getByPlaceholderText("أضف مهمة جديدة..."),
{ target: { value: "مهمة للحذف" } });
fireEvent.click(screen.getByText("إضافة"));
// حذفها
fireEvent.click(screen.getByText("حذف"));
expect(screen.queryByText("مهمة للحذف")).not.toBeInTheDocument();
});
test("تبديل حالة المهمة", () => {
render(<TodoApp />);
fireEvent.change(screen.getByPlaceholderText("أضف مهمة جديدة..."),
{ target: { value: "مهمة اختبار" } });
fireEvent.click(screen.getByText("إضافة"));
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
});
});
أفضل الممارسات في React
- مكون واحد لكل ملف: حافظ على كل مكون في ملف مستقل مع اسم وصفي واضح يبدأ بحرف كبير ويعبّر عن وظيفة المكون
- رفع الحالة لأعلى (Lifting State Up): عندما يحتاج مكونان أخوان لنفس البيانات، ارفع الحالة إلى أقرب أب مشترك بدلاً من تكرارها
- فصل المنطق عن العرض: استخدم Custom Hooks لفصل منطق الأعمال عن مكونات العرض مما يسهّل الاختبار وإعادة الاستخدام
- تجنب الحالة غير الضرورية: إذا أمكن حساب قيمة من حالة موجودة، لا تنشئ حالة جديدة لها. استخدم المتغيرات المحلية أو useMemo بدلاً من ذلك
- استخدم TypeScript: إضافة أنواع البيانات الثابتة يمنع الكثير من الأخطاء ويحسّن تجربة التطوير مع الإكمال التلقائي ورسائل خطأ أوضح
- تنظيم المجلدات: اتبع هيكلاً واضحاً مثل تقسيم الملفات إلى مجلدات components و pages و hooks و utils و services و styles
ملخص المهارات المكتسبة
- إعداد مشروع React حديث باستخدام Vite مع هيكل منظم
- فهم وكتابة JSX مع العرض الشرطي والقوائم الديناميكية
- بناء مكونات قابلة لإعادة الاستخدام مع Props و Children
- إدارة الحالة المحلية باستخدام useState للنماذج والعدادات
- التعامل مع التأثيرات الجانبية وجلب البيانات باستخدام useEffect
- إنشاء Custom Hooks لمشاركة المنطق بين المكونات
- إعداد التنقل بين الصفحات باستخدام React Router
- بناء تطبيق كامل بميزات CRUD والتصفية
الخطوة التالية
بعد إتقان أساسيات React، انتقل إلى تعلم إدارة الحالة المتقدمة مع Context API و Redux Toolkit. استكشف Next.js لبناء تطبيقات React مع التصيير من جانب الخادم (SSR) وتوليد المواقع الثابتة (SSG). تعلم React Query/TanStack Query لإدارة بيانات الخادم بكفاءة، و TypeScript مع React لتحسين جودة الكود في المشاريع الكبيرة.