React: واجهات من مكوّنات
درس 5 من 6
5React: واجهات من مكوّنات
الصورة الكبيرةفي درس DOM كتبت render() بيدك، وأعدت رسم القائمة كاملة مع كل تغيير. في تطبيق فيه مئة شاشة يصير هذا صعبًا. React يأخذ نفس الفكرة ويؤتمتها: تصف شكل الواجهة لكل حالة، وهو يحسب أقل تعديل على الصفحة ويطبّقه. لهذا صار أشهر مكتبة واجهات في العالم.
ماذا تشاهد في الرسم: شجرة مكوّنات: البيانات تنزل من App عبر props، والأحداث تصعد عبر دوال تغيّر الحالة، فيرسم React من جديد ما تغيّر فقط.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
React مثل قطع الليغو: تبني «زرًّا» مرة وتستخدمه في كل مكان، وتبني «بطاقة منتج» من زر وصورة ونص، وتبني «صفحة» من بطاقات. كل قطعة اسمها مكوّن. وبدل أن تقول للصفحة «احذف هذا وأضف ذاك وغيّر العدّاد»، تقول لـ React «هذه قائمة المهام الآن» وهو يرتّب الصفحة لتطابقها، مثل مساعد يقارن صورتين ويغيّر الفرق فقط.
React مكتبة واجهات تصريحية: المكوّن دالة نقية تقريبًا من props وحالة إلى عناصر React (كائنات خفيفة تصف DOM). JSX سكّر نحوي يحوّله المترجم (Babel أو esbuild في Vite) لاستدعاءات jsx()/createElement. عند تغيّر الحالة يعيد React استدعاء المكوّن، ويقارن الشجرة الجديدة بالقديمة (reconciliation) ثم يطبّق الفرق على DOM. useState يحفظ حالة بين عمليات الرسم ويجدول إعادة رسم، والتحديث يجب أن يكون غير معدّل (immutable) لأن React يقارن بالمرجع (Object.is). المفاتيح key تحدّد هوية عناصر القائمة في المصالحة. useEffect للتزامن مع أنظمة خارجية بمصفوفة اعتماديات ودالة تنظيف، وفي وضع StrictMode يُشغَّل مرتين في التطوير ليكشف الآثار غير النظيفة. القواعد: الخطّافات (hooks) تُنادى في أعلى المكوّن فقط، لا داخل شرط أو حلقة.
قبل أن تبدأ
🖱️JavaScript والـ DOM
بنيت قائمة المهام بـ render يدوي.
⏳JavaScript غير المتزامن
تعرف fetch وasync/await وحالات التحميل.
📦Node.js وnpm
مثبّتان لتشغيل npm create vite@latest، من درس Express.
الفكرة الجوهرية
1لماذا React؟ من render اليدوي إلى UI = f(state)
تذكّر قائمة المهام في درس DOM: كل حدث يغيّر الحالة، ثم render() تبني القائمة كاملة من جديد. هذا صحيح لكنه مكلف وطويل: في تطبيق كبير تكتب آلاف أسطر createElement. React يقول: صِف الواجهة كدالة تأخذ الحالة وتعيد الشكل، وأنا أتكفّل بالباقي. المثال التالي يحاكي هذه الفكرة بـ JavaScript عادي، لترى أن React ليس سحرًا:
// a component is just a function: state in, description out
const Item = (todo) => `<li${todo.done ? ' class="done"' : ""}>${todo.text}</li>`;
const App = (state) => `<ul>${state.todos.map(Item).join("")}</ul><p>${state.todos.filter((t) => !t.done).length} left</p>`;
let state = { todos: [{ text: "buy beans", done: false }] };
let screen = App(state);
console.log("first render:\n ", screen);
// an event: we never touch the screen, only the state
state = { todos: [...state.todos, { text: "grind coffee", done: true }] };
const next = App(state);
// React compares the old and new description and applies only the difference
console.log("changed?", next !== screen);
console.log("new render:\n ", next);هذه هي فكرة React كلها: الواجهة = دالة(الحالة). أنت تغيّر الحالة فقط، وReact يعيد استدعاء دوال المكوّنات، يقارن النتيجة بالسابقة، ويعدّل في DOM الحقيقي العناصر التي تغيّرت فقط.
2أنشئ مشروعك الأول بـ Vite
Vite أداة تنشئ لك مشروع React جاهزًا بأمر واحد وتشغّله بخادم تطوير يحدّث المتصفح لحظة الحفظ. في الطرفية:
npm create vite@latest cafe -- --template react cd cafe npm install npm run dev # open http://localhost:5173
هيكل المشروع: صفحة HTML واحدة فيها عنصر فارغ، وReact يملؤه بالمكوّنات.
cafe/
index.html <- the single HTML page, with <div id="root">
package.json <- dependencies and scripts (dev, build)
src/
main.jsx <- mounts <App /> into #root
App.jsx <- your first component: edit it and save
App.css3المكوّن وJSX
المكوّن دالة يبدأ اسمها بحرف كبير وتعيد JSX: شكل يشبه HTML مكتوب داخل JavaScript. تستخدمه كوسم: <Price />. فروق JSX عن HTML: className بدل class، وhtmlFor بدل for، وأي قيمة JavaScript بين أقواس معقوفة {}، وكل وسم يُغلق حتى الفارغ (<img />)، والمكوّن يعيد عنصرًا أبًا واحدًا (أو <>...</> إن لم ترد غلافًا).
function Price({ amount, currency = "SAR" }) {
return <span className="price">{amount} {currency}</span>;
}
function ProductCard({ name, amount, image }) {
return (
<article className="card">
<img src={image} alt={name} />
<h3>{name}</h3>
<Price amount={amount} />
</article>
);
}
export default function App() {
return (
<main>
<ProductCard name="Latte" amount={14} image="/latte.jpg" />
<ProductCard name="Mocha" amount={16} image="/mocha.jpg" />
</main>
);
}JSX ليس سحرًا: المترجم يحوّل كل وسم لاستدعاء دالة createElement(النوع، الخصائص، الأبناء). الملعب التالي يحمّل React الحقيقي من الإنترنت ويستخدم createElement مباشرة بلا أي أداة، فترى ما يحدث تحت JSX:
React حقيقي يعمل هنا
اضغط + و− في كل عدّاد: لكل نسخة من المكوّن حالتها المستقلة. أضف عدّادًا ثالثًا لـ «V60» بسطر واحد. (يحتاج اتصالًا بالإنترنت لتحميل React.)
4props: مدخلات المكوّن والتركيب
props هي ما يمرّره الأب للمكوّن، مثل معاملات الدالة: <Price amount={14} /> يصل كـ { amount: 14 }. وهي للقراءة فقط: المكوّن لا يغيّر props التي استلمها. وخاصية children الخاصة تحمل ما بين وسمي الفتح والإغلاق، فتبني مكوّنات «أغلفة» تقبل أي محتوى.
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
<Card title="Today's offer">
<p>Two lattes for 20 SAR.</p>
<button>Order</button>
</Card>5الحالة: useState
const [count, setCount] = useState(0) يعطيك قيمة ودالة لتغييرها. حين تنادي setCount(5) يتذكّر React القيمة الجديدة ويعيد رسم المكوّن بها. والحالة محلية: كل نسخة من المكوّن لها حالتها. وحين تعتمد القيمة الجديدة على القديمة، مرّر دالة: setCount((c) => c + 1).
import { useState } from "react";
function Counter() {
const [cups, setCups] = useState(1);
return (
<div>
<button onClick={() => setCups((c) => Math.max(1, c - 1))}>-</button>
<output>{cups}</output>
<button onClick={() => setCups((c) => c + 1)}>+</button>
<p>Total: {cups * 14} SAR</p>
</div>
);
}لماذا لا نعدّل الحالة مباشرة؟ React يكتشف التغيير بمقارنة المرجع: هل هي نفس المصفوفة أم مصفوفة جديدة؟ شغّل هذا لترى الفرق:
const todos = ["buy beans"];
// wrong: push changes the same array, so the reference is identical
const mutated = todos;
mutated.push("grind coffee");
console.log("push -> same reference?", mutated === todos, "(React sees no change)");
// right: spread builds a new array, so React knows to redraw
const fresh = [...todos, "brew"];
console.log("spread -> same reference?", fresh === todos, "(React redraws)");todos.push(newTodo); setTodos(todos). المصفوفة نفس المرجع فلا يلاحظ React أي تغيير ولا يرسم. اكتب setTodos([...todos, newTodo])، وللتعديل map، وللحذف filter.6الأحداث والنماذج المتحكَّم بها
الأحداث في JSX خصائص بحرف كبير: onClick وonChange وonSubmit، وتعطيها دالة لا نداء دالة (onClick={add} لا onClick={add()}). والحقل المتحكَّم به (controlled) قيمته من الحالة (value={text}) ويحدّثها مع كل حرف (onChange)، فتبقى الحالة والشاشة متطابقتين دائمًا وتستطيع أن تتحقّق أو تنسّق أثناء الكتابة.
function SignUp() {
const [email, setEmail] = useState("");
const valid = email.includes("@");
function handleSubmit(e) {
e.preventDefault(); // same as the DOM lesson
if (valid) console.log("send", email);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
{!valid && email && <p className="error">Email needs an @</p>}
<button disabled={!valid}>Sign up</button>
</form>
);
}onClick={remove(todo.id)} ينفّذ الحذف فورًا أثناء الرسم لا عند الضغط. الصحيح أن تلفّه في دالة: onClick={() => remove(todo.id)}.7القوائم والمفاتيح والعرض المشروط
ترسم القائمة بـ map تعيد JSX لكل عنصر، وكل عنصر يحتاج key ثابتًا وفريدًا من البيانات (معرّف، لا رقم الترتيب). المفتاح يجعل React يعرف أي عنصر حُذف أو تحرّك فيحافظ على حالته الصحيحة. وللعرض المشروط: {شرط && <X />} يعرض X إن صحّ الشرط، و{شرط ? <A /> : <B />} يختار بينهما.
function Menu({ drinks }) {
if (drinks.length === 0) return <p>The menu is empty today.</p>;
return (
<ul>
{drinks.map((d) => (
<li key={d.id}>
{d.name} · {d.price} SAR
{d.soldOut ? <em> (sold out)</em> : <button>Add</button>}
</li>
))}
</ul>
);
}8رفع الحالة: من يملك البيانات؟
إن احتاج مكوّنان نفس البيانات (النموذج يضيف مهمة، والقائمة تعرضها)، ضع الحالة في أقرب أب مشترك لهما، ومرّرها للأسفل كـ props، ومرّر معها دوال تغيّرها. هذا اسمه «رفع الحالة». القاعدة: البيانات تنزل، والأحداث تصعد.
App <- owns: todos, and the functions add / toggle / remove
/ \
TodoForm TodoList props down: todos, onToggle, onDelete
onAdd(text) ↑ \ events up: onAdd("grind coffee")
TodoItem × n9useEffect: جلب البيانات والتزامن مع الخارج
ما ليس رسمًا (جلب بيانات من الخادم، حفظ في localStorage، مؤقّت، اشتراك) يذهب في useEffect. يعمل بعد الرسم، والمصفوفة الثانية تحدّد متى يعيد التشغيل: [] مرة عند الظهور، و[todos] كلما تغيّرت المهام. والدالة التي يعيدها (اختيارية) تنظّف قبل التشغيل التالي أو عند الإخفاء. مع fetch تحتاج ثلاث حالات كما في درس غير المتزامن: البيانات، والتحميل، والخطأ.
function MenuPage() {
const [drinks, setDrinks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch("/api/menu", { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`server said ${res.status}`);
return res.json();
})
.then(setDrinks)
.catch((err) => { if (err.name !== "AbortError") setError(err.message); })
.finally(() => setLoading(false));
return () => controller.abort(); // cleanup: cancel if we leave the page
}, []); // [] = run once
if (loading) return <p>Loading the menu...</p>;
if (error) return <p role="alert">Could not load: {error}</p>;
return <Menu drinks={drinks} />;
}مثال محلول
قائمة المهام كمكوّنات: الحالة في App، والنموذج يرفع النص للأعلى بدالة onAdd، والقائمة ترسم العناصر بمفاتيح، وuseEffect يحفظ في localStorage. قارنها سطرًا بسطر مع نسخة درس DOM: نفس المنطق تمامًا، لكن بلا createElement ولا replaceChildren.
import { useEffect, useState } from "react";
function TodoForm({ onAdd }) {
const [text, setText] = useState("");
return (
<form onSubmit={(e) => { e.preventDefault(); if (text.trim()) onAdd(text.trim()); setText(""); }}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="New task" aria-label="New task" />
<button>Add</button>
</form>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className={todo.done ? "done" : ""}>
<label>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
</label>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
export default function App() {
const [todos, setTodos] = useState(() => JSON.parse(localStorage.getItem("todos") ?? "[]"));
const [filter, setFilter] = useState("all");
useEffect(() => localStorage.setItem("todos", JSON.stringify(todos)), [todos]);
const add = (text) => setTodos((t) => [...t, { id: crypto.randomUUID(), text, done: false }]);
const toggle = (id) => setTodos((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
const remove = (id) => setTodos((t) => t.filter((x) => x.id !== id));
const shown = todos.filter((t) => filter === "all" || (filter === "done" ? t.done : !t.done));
return (
<main>
<TodoForm onAdd={add} />
<div role="group" aria-label="Filter">
{["all", "done", "left"].map((f) => (
<button key={f} aria-pressed={filter === f} onClick={() => setFilter(f)}>{f}</button>
))}
</div>
<ul>
{shown.map((t) => <TodoItem key={t.id} todo={t} onToggle={toggle} onDelete={remove} />)}
</ul>
<p>{todos.filter((t) => !t.done).length} left</p>
</main>
);
}خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| React إطار كامل يتكفّل بكل شيء. | هو مكتبة واجهات. التوجيه وجلب البيانات والخادم من أطر فوقه مثل Next.js. |
| رقم الترتيب يكفي كمفتاح. | ينكسر حين تحذف أو ترتّب، فتنتقل حالة عنصر لآخر. استخدم معرّفًا من البيانات. |
| تعلّم React مباشرة وتجاوز JavaScript. | React هو JavaScript: map وfilter والنشر والدوال السهمية في كل سطر. من يتجاوزها يتعثّر. |
أين تقابله في العمل
🏢منتجات كبرى
فيسبوك وإنستغرام ونتفليكس وكثير من التطبيقات المحلية.
📱React Native
نفس الأفكار تبني تطبيقات جوال حقيقية لأندرويد وiOS.
💼سوق العمل
أكثر مهارة واجهات مطلوبة في إعلانات الوظائف.
اختبر فهمك
أضفت عنصرًا بـ todos.push(x) ثم setTodos(todos) ولم تتحدّث الصفحة. لماذا؟
React يقارن المراجع. أنشئ مصفوفة جديدة بالنشر.
أين تضع جلب البيانات من الخادم في مكوّن React؟
الجلب أثر جانبي خارج الرسم، ومكانه useEffect.
كيف يرسل مكوّن ابن بيانات لأبيه؟
البيانات تنزل كـ props، والأحداث تصعد بدوال مثل onAdd.
ما الخطأ في <button onClick={save()}>؟
مرّر الدالة نفسها أو لفّها: onClick={() => save()}.
ما فائدة key في القائمة؟
بالمفتاح يطابق React العناصر بين رسمتين ويحافظ على حالتها.
بطاقات مراجعة سريعة
تحقق من فهمك
ما معنى UI = f(state)؟
الواجهة نتيجة حتمية للحالة. تغيّر الحالة فقط، وReact يعيد حساب الواجهة.
لماذا لا نعدّل props داخل الابن؟
لأنها ملك الأب ومصدرها حالته. الابن يطلب التغيير بدالة، والأب يغيّر حالته.
ماذا يحدث لو نسيت مصفوفة الاعتماديات في useEffect؟
يعمل الأثر بعد كل رسم، وقد يدخل في حلقة لا تنتهي لو غيّر الحالة.
ما الحقل المتحكَّم به؟
حقل قيمته من الحالة ويحدّثها مع كل تغيير، فالحالة هي مصدر الحقيقة لا DOM.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.