مقدمة عن المكونات

المكونات (Components) هي اللبنات الأساسية في تطبيقات React وNext.js. وهي وحدات قابلة لإعادة الاستخدام تمكنك من تقسيم واجهة المستخدم إلى أجزاء مستقلة ومعزولة.

لماذا نستخدم المكونات؟

  • إعادة الاستخدام: يمكن استخدام نفس المكون في أماكن مختلفة دون تكرار الكود
  • التنظيم: تجزئة التطبيق المعقد إلى أجزاء أصغر وأكثر إدارة
  • الصيانة: تسهيل عملية صيانة وتحديث الكود عندما يكون منظمًا في وحدات منفصلة
  • الاختبار: سهولة اختبار مكونات منفصلة بدلاً من اختبار تطبيق كامل
  • التعاون: يمكن لفرق مختلفة العمل على مكونات مختلفة بشكل متوازٍ

المفاهيم الأساسية للمكونات

قبل التعمق في المكونات المختلفة، من المهم فهم المفاهيم الأساسية:

Props

بيانات تمرر إلى المكون من المكون الأب، وهي للقراءة فقط (read-only) ولا يمكن تغييرها داخل المكون.

State

بيانات خاصة بالمكون ويمكن تغييرها، وعند تغييرها يتم إعادة رندر المكون لعرض التغييرات.

Lifecycle

دورة حياة المكون من لحظة إنشائه حتى إزالته، تتيح تنفيذ كود في مراحل معينة.

JSX

امتداد للغة JavaScript يتيح كتابة عناصر HTML داخل كود JavaScript بشكل سهل وواضح.

في الأقسام التالية، سنستعرض المكونات المختلفة في React وNext.js، وكيفية استخدامها، ومتى يجب استخدام كل منها.

مكونات React الأساسية

React يوفر أنواعًا مختلفة من المكونات لبناء تطبيقك. لنتعرف على الأنواع الرئيسية وكيفية استخدامها.

المكونات الوظيفية (Functional Components)

المكونات الوظيفية هي دوال JavaScript تقبل props كمُدخل وتعيد JSX. وهي الطريقة المفضلة حاليًا لإنشاء المكونات في React.

import React from 'react';

// مكون وظيفي بسيط
function Greeting({ name }) {
  return <h1>مرحبًا، {name}!</h1>;
}

// مكون وظيفي باستخدام سهم الدالة
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
};

// تصدير المكونات
export { Greeting, Button };

مميزات المكونات الوظيفية:

  • أبسط وأسهل للفهم والكتابة
  • أقل كود مقارنة بمكونات الفئة
  • أداء أفضل في معظم الحالات
  • تدعم Hooks (منذ React 16.8)
  • سهولة اختبارها

الاستخدامات:

استخدم المكونات الوظيفية في معظم الحالات، خاصة للمكونات البسيطة التي تعرض بيانات أو تستقبل تفاعلات بسيطة.

مكونات الفئة (Class Components)

مكونات الفئة هي فئات JavaScript تمتد من React.Component. كانت هي الطريقة الرئيسية لإنشاء مكونات React قبل إصدار Hooks.

import React, { Component } from 'react';

// مكون فئة بسيط
class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  }
  
  render() {
    return (
      <div>
        <p>العدد: {this.state.count}</p>
        <button onClick={this.increment}>زيادة</button>
      </div>
    );
  }
}

export default Counter;

مميزات مكونات الفئة:

  • تدعم كل دورة حياة React بشكل كامل
  • تدير الحالة بشكل رسمي باستخدام this.state وthis.setState
  • تناسب المشاريع القديمة التي لا تستخدم Hooks

الفرق بين المكونات الوظيفية ومكونات الفئة

الخاصية المكونات الوظيفية مكونات الفئة
الكتابة دوال JavaScript عادية فئات ES6 تمتد من React.Component
إدارة الحالة useState Hook this.state و this.setState
دورة الحياة useEffect وغيرها من Hooks طرق دورة الحياة مثل componentDidMount
this لا يستخدم this يتطلب فهم this وربطه بالدوال

React Fragments

Fragments تسمح بتجميع قائمة من العناصر الفرعية دون إضافة عنصر إضافي إلى DOM. هذا مفيد عندما تحتاج إلى إرجاع عناصر متعددة من مكون.

import React, { Fragment } from 'react';

// استخدام Fragment بشكل صريح
function Table() {
  return (
    <table>
      <tbody>
        <tr>
          <Fragment>
            <td>بند 1</td>
            <td>بند 2</td>
          </Fragment>
        </tr>
      </tbody>
    </table>
  );
}

// استخدام الصيغة المختصرة للـ Fragment
function ListItems() {
  return (
    <>
      <li>العنصر الأول</li>
      <li>العنصر الثاني</li>
      <li>العنصر الثالث</li>
    </>
  );
}

لماذا نستخدم Fragments؟

  • تجنب إضافة عناصر DOM غير ضرورية
  • تحسين الأداء وتقليل استهلاك الذاكرة
  • الحفاظ على هيكل DOM صحيح (مثلاً: في الجداول حيث لا يمكن وضع div داخل tr)
  • تحسين قابلية قراءة الكود عند إرجاع عناصر متعددة

React Portals

Portals توفر طريقة لرسم عنصر React في عقدة DOM خارج التسلسل الهرمي للمكون الأب. هذا مفيد لعناصر مثل النوافذ المنبثقة والحوارات وإشعارات التوست.

import React from 'react';
import ReactDOM from 'react-dom';

function Modal({ children, isOpen, onClose }) {
  if (!isOpen) return null;
  
  // إنشاء portal يضيف المحتوى إلى عنصر خارج التسلسل الهرمي للمكون
  return ReactDOM.createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        <button onClick={onClose}>إغلاق</button>
        {children}
      </div>
    </div>,
    // تحديد عنصر DOM الهدف الذي سيتم إرسال المحتوى إليه
    document.getElementById('modal-root')
  );
}

لاستخدام المكون أعلاه، تحتاج إلى عنصر في ملف HTML الخاص بك:

<!-- في ملف HTML -->
<div id="root"></div> 
<div <            "root"></div> 
<div id="modal-root"></div> 

متى تستخدم Portals؟

  • النوافذ المنبثقة (Modals) والحوارات (Dialogs)
  • إشعارات التوست (Toast notifications)
  • تلميحات الأدوات (Tooltips)
  • عندما تحتاج إلى تجاوز خصائص CSS مثل overflow: hidden أو z-index من المكون الأب

بالرغم من أن العنصر موجود خارج التسلسل الهرمي للـ DOM، إلا أنه يظل ضمن نفس شجرة React، مما يعني أن الأحداث ستنتشر بشكل طبيعي.

React Hooks

Hooks هي ميزة أضيفت في React 16.8 تتيح لك استخدام حالة ومزايا React الأخرى دون كتابة مكون فئة. الـ Hooks تسمح لك بإعادة استخدام منطق الحالة بين المكونات دون تغيير هيكلها.

useState

يسمح للمكونات الوظيفية بإدارة الحالة المحلية.

import React, { useState } from 'react';

function Counter() {
  // تهيئة متغير state مع قيمة أولية 0
  const [count, setCount] = useState(0);
  
  // زيادة العداد
  const increment = () => {
    setCount(count + 1);
  };
  
  // زيادة العداد باستخدام الدالة التحديثية (للقيم المعتمدة على الحالة السابقة)
  const safeIncrement = () => {
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>العدد: {count}</p>
      <button onClick={increment}>زيادة</button>
      <button onClick={safeIncrement}>زيادة آمنة</button>
    </div>
  );
}

نقاط مهمة عن useState:

  • يمكن استخدامه أكثر من مرة في نفس المكون لمتغيرات حالة مستقلة
  • عند تغيير الحالة، يتم إعادة رندر المكون كاملاً
  • القيمة الأولية تُستخدم فقط في الرندر الأول
  • لا تقم بتحديث الحالة مباشرة (count = count + 1)، استخدم دائمًا دالة التحديث (setCount)
  • عند التحديث المعتمد على القيمة السابقة، استخدم الشكل الوظيفي: setCount(prevCount => prevCount + 1)

useEffect

يسمح بإجراء تأثيرات جانبية في المكونات الوظيفية. يمكن استخدامه لمحاكاة أساليب دورة الحياة مثل componentDidMount وcomponentDidUpdate وcomponentWillUnmount.

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // يتم تنفيذه بعد الرندر واستكمال التغييرات على DOM
  useEffect(() => {
    // محاكاة طلب API
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching data:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    // وظيفة التنظيف - تنفذ عند إزالة المكون (componentWillUnmount)
    return () => {
      // إلغاء الطلبات أو تنظيف الموارد
      console.log('تنظيف الموارد');
    };
  }, []); // مصفوفة التبعيات فارغة - تنفيذ useEffect مرة واحدة فقط
  
  if (loading) return <p>جاري التحميل...</p>;
  
  return (
    <div>
      <h2>البيانات:</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

أنماط استخدام useEffect:

تنفيذ مع كل رندر
useEffect(() => {
  // ينفذ بعد كل رندر
});
تنفيذ مرة واحدة فقط (عند التركيب)
useEffect(() => {
  // ينفذ مرة واحدة بعد الرندر الأول
}, []);
تنفيذ عند تغير قيم محددة
useEffect(() => {
  // ينفذ عند تغير id أو name
}, [id, name]);
تنفيذ مع التنظيف
useEffect(() => {
  // التأثير
  return () => {
    // التنظيف
  };
}, [dependencies]);

useContext

يسمح بالوصول إلى القيم من React Context دون استخدام مكونات المستهلك (Consumer components).

import React, { createContext, useContext, useState } from 'react';

// إنشاء السياق
const ThemeContext = createContext();

// مكون المزود - يوفر القيم للمكونات الفرعية
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// مكون فرعي يستخدم السياق
function ThemedButton() {
  // استخدام useContext للوصول إلى قيم السياق
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        padding: '10px',
        borderRadius: '4px'
      }}
    >
      تبديل النمط ({theme})
    </button>
  );
}

// استخدام المكونات
function App() {
  return (
    <ThemeProvider>
      <div>
        <h1>استخدام Context</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

نقاط مهمة عن useContext:

  • يبسط العمل مع Context من خلال إلغاء الحاجة إلى استخدام Consumer Components
  • يعتبر بديلاً عن نقل Props عبر مستويات متعددة (prop drilling)
  • مناسب للبيانات العالمية مثل المصادقة، الموضوع، التفضيلات اللغوية، إلخ
  • عند تغيير قيمة Context، ستتم إعادة رندر جميع المكونات التي تستخدم تلك القيمة

useRef

يسمح بالاحتفاظ بقيمة مرجعية قابلة للتغيير دون إعادة رندر المكون عند تغييرها، ويستخدم أيضًا للوصول إلى عناصر DOM مباشرة.

import React, { useRef, useEffect, useState } from 'react';

function TextInputWithFocusButton() {
  // إنشاء مرجع لعنصر input
  const inputRef = useRef(null);
  
  // تركيز حقل الإدخال عند النقر على الزر
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>تركيز الإدخال</button>
    </div>
  );
}

function Counter() {
  const [count, setCount] = useState(0);
  
  // استخدام useRef لتخزين متغير لا يتسبب في إعادة الرندر عند تغييره
  const renderCount = useRef(1);
  
  useEffect(() => {
    // زيادة عدد مرات الرندر بعد كل تحديث
    renderCount.current = renderCount.current + 1;
  });
  
  return (
    <div>
      <p>العدد: {count}</p>
      <p>عدد مرات الرندر: {renderCount.current}</p>
      <button onClick={() => setCount(c => c + 1)}>زيادة</button>
    </div>
  );
}

استخدامات useRef:

  • الوصول إلى عناصر DOM مباشرة (focus، measurements، إلخ)
  • تخزين قيم متغيرة دون إعادة رندر المكون
  • تخزين القيم السابقة للحالة أو الخصائص
  • تتبع الفترات الزمنية (setInterval/setTimeout) لتنظيفها لاحقًا

useMemo

يتيح تخزين نتائج العمليات الحسابية المكلفة وإعادة استخدامها فقط عند تغير القيم ذات الصلة.

import React, { useMemo, useState } from 'react';

function ExpensiveCalculation({ list, filter }) {
  // حساب مكلف - تطبيق فلتر على قائمة كبيرة
  const filteredList = useMemo(() => {
    console.log('تنفيذ الحساب المكلف...');
    
    return list.filter(item => item.includes(filter));
  }, [list, filter]); // إعادة الحساب فقط عند تغير القائمة أو الفلتر
  
  return (
    <div>
      <p>القائمة المفلترة:</p>
      <ul>
        {filteredList.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

function MemoExample() {
  const [filter, setFilter] = useState('');
  const [count, setCount] = useState(0);
  
  // قائمة كبيرة افتراضية
  const largeList = new Array(1000)
    .fill()
    .map((_,i) => `عنصر ${i + 1}`);
  
  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="تصفية..."
      />
      
      <button onClick={() => setCount(c => c + 1)}>
        زيادة العداد: {count}
      </button>
      
      <ExpensiveCalculation list={largeList} filter={filter} />
    </div>
  );
}

نقاط مهمة عن useMemo:

  • يمنع إعادة الحسابات المكلفة عند كل رندر
  • يجب استخدامه فقط للعمليات الحسابية المكلفة
  • القيمة المخزنة مؤقتًا تتم إعادة حسابها فقط عند تغير إحدى التبعيات
  • استخدامه الزائد قد يؤدي إلى تعقيد الكود وتأثير سلبي على الأداء

useCallback

يقوم بتخزين وإعادة استخدام تعريفات الدوال عبر عمليات الرندر، مما يساعد في تحسين الأداء لتجنب إعادة إنشاء الدوال في كل مرة.

import React, { useState, useCallback } from 'react';

// مكون فرعي يعتمد على دالة callback
const ChildComponent = React.memo(({ onClick }) => {
  console.log('تم رندر المكون الفرعي');
  return <button onClick={onClick}>انقر هنا</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // بدون useCallback: سيتم إنشاء هذه الدالة في كل مرة يتم رندر المكون
  // const handleClick = () => {
  //   console.log('تم النقر!');
  // };
  
  // مع useCallback: يتم إعادة استخدام نفس الدالة طالما لم تتغير المتغيرات المستخدمة فيها
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // مصفوفة تبعيات فارغة - الدالة لا تعتمد على أي متغيرات خارجية
  
  return (
    <div>
      <p>العدد: {count}</p>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        placeholder="تغيير هذا لن يعيد إنشاء handleClick"
      />
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

الفرق بين useMemo وuseCallback:

useMemo useCallback
يخزن نتيجة دالة يخزن الدالة نفسها
useMemo(() => calculation(a, b), [a, b]); useCallback(() => doSomething(a, b), [a, b]);
مفيد لتخزين قيم مكلفة الحساب مفيد عند تمرير دوال إلى مكونات فرعية محسنة

Hooks مخصصة (Custom Hooks)

تمكنك من استخراج منطق الحالة المشترك بين المكونات واستخدامه في مكونات متعددة. Hooks المخصصة هي دوال JavaScript تبدأ بـ "use" وقد تستخدم hooks أخرى داخلها.

import { useState, useEffect } from 'react';

// Hook مخصص لاسترجاع البيانات من API
function useFetch(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
  
    useEffect(() => {
      let isMounted = true;
      
      const fetchData = async () => {
        setLoading(true);
        
        try {
          const response = await fetch(url);
          
          if (!response.ok) {
            throw new Error(`فشل الطلب: ${response.status}`);
          }
          
          const result = await response.json();
          
          if (isMounted) {
            setData(result);
            setError(null);
          }
        } catch (err) {
          if (isMounted) {
            setError(err.message);
            setData(null);
          }
        } finally {
          if (isMounted) {
            setLoading(false);
          }
        }
      };
      
      fetchData();
      
      return () => {
        isMounted = false;
      };
    }, [url]);
    
    return { data, loading, error };
  }
  
  // Hook مخصص لإدارة حالة النموذج
  function useForm(initialValues = {}) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    
    const handleChange = (e) => {
      const { name, value } = e.target;
      setValues(prevValues => ({
        ...prevValues,
        [name]: value
      }));
    };
    
    const reset = () => {
      setValues(initialValues);
      setErrors({});
    };
    
    return { values, errors, setErrors, handleChange, reset };
  }
  
  // مثال على استخدام الـ Hooks المخصصة
  function UserProfile({ userId }) {
    const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
    
    if (loading) return <p>جاري التحميل...</p>;
    if (error) return <p>خطأ: {error}</p>;
    
    return (
      <div>
        <h2>{data.name}</h2>
        <p>البريد الإلكتروني: {data.email}</p>
      </div>
    );
  }

مزايا استخدام Hooks المخصصة:

  • استخراج وإعادة استخدام المنطق المشترك بين المكونات
  • تبسيط المكونات وتقليل التكرار
  • تنظيم الكود بشكل أفضل وتسهيل اختباره
  • مشاركة الحالة والمنطق دون تغيير هيكل المكونات

قواعد إنشاء Hooks مخصصة:

  • يجب أن يبدأ اسم Hook المخصص بـ "use" (مثل useForm، useFetch، إلخ)
  • يمكن استخدام Hooks أخرى داخل Hook المخصص
  • يجب اتباع قواعد Hooks (استخدامها في المستوى الأعلى فقط وليس داخل الشروط أو الحلقات)
  • يمكن إرجاع أي قيمة من Hook المخصص (object، array، primitive، إلخ)

مكونات Next.js

يوفر Next.js مجموعة من المكونات المدمجة التي تساعد في بناء تطبيقات ويب متقدمة وتحسين الأداء وتجربة المستخدم.

Image

مكون Image من Next.js يعمل على تحسين أداء الصور تلقائيًا، مع دعم التحميل الكسول (lazy loading) وتغيير الحجم حسب الجهاز.

import Image from 'next/image';
  
  function ProductCard({ product }) {
    return (
      <div className="product-card">
        // استخدام مكون Image بدلًا من الـ img العادي
        <Image
          src={product.imageUrl}
          alt={product.name}
          width={500}
          height={300}
          quality={75} // جودة الصورة (افتراضي: 75)
          placeholder="blur" // إظهار نسخة مشوشة أثناء التحميل
          blurDataURL={product.blurDataUrl} // بيانات الصورة المشوشة
          priority={product.isFeatured} // أولوية التحميل للصور المهمة
        />
        <h3>{product.name}</h3>
        <p>{product.price}</p>
      </div>
    );
  }
  
  // استخدام الصور المستضافة خارجيًا
  function ExternalImage() {
    return (
      <Image
        src="https://example.com/image.jpg"
        alt="وصف الصورة"
        width={800}
        height={600}
        unoptimized // لتجاوز التحسين للصور الخارجية
      />
    );
  }

مزايا مكون Image:

  • منع تحركات المحتوى أثناء تحميل الصور (CLS)
  • تحميل الصور عند الحاجة (Lazy Loading)
  • تقديم الصور بتنسيقات حديثة مثل WebP وAVIF حسب دعم المتصفح
  • تغيير حجم الصور تلقائيًا حسب الجهاز
  • دعم الصور المشوشة أثناء التحميل (blurred placeholder)
  • تحسين أداء الصفحة وتقليل وقت التحميل

Script

مكون Script يتيح تحميل النصوص البرمجية الخارجية (JavaScript) بشكل محسن وآمن، مع التحكم في توقيت التحميل والتنفيذ.

import Script from 'next/script';
  
  function AnalyticsPage() {
    return (
      <>
        // تحميل Google Analytics
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
          strategy="afterInteractive" // تحميل بعد جعل الصفحة تفاعلية
          onLoad={() => console.log('تم تحميل Google Analytics')}
        />
        
        // تنفيذ كود inline
        <Script id="analytics-config" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'GA_MEASUREMENT_ID');
          `}
        </Script>
        
        // محتوى الصفحة
        <div>
          <h1>الصفحة الرئيسية</h1>
        </div>
      </>
    );
  }

استراتيجيات تحميل Script:

  • beforeInteractive: تحميل قبل أن تصبح الصفحة تفاعلية (مناسب للنصوص المهمة)
  • afterInteractive (افتراضي): تحميل بعد أن تصبح الصفحة تفاعلية
  • lazyOnload: تحميل بعد اكتمال الموارد الأخرى والأوقات الخاملة للمتصفح
  • worker (تجريبي): تحميل في web worker

مكونات التوجيه

يوفر Next.js نظامين للتوجيه: App Router (الإصدار 13 وما بعده) وPages Router (الإصدار القديم). كل منهما له مكوناته وأساليبه الخاصة.

App Router

يعتمد App Router على مجلد app/ وملفات خاصة (special files) لتعريف المسارات والسلوك.

// app/page.tsx - الصفحة الرئيسية "/"
  export default function HomePage() {
    return <h1>الصفحة الرئيسية</h1>;
  }
  
  // app/blog/page.tsx - صفحة المدونة "/blog"
  export default function BlogPage() {
    return <h1>المدونة</h1>;
  }
  
  // app/blog/[slug]/page.tsx - صفحة مقال محدد "/blog/article-1"
  export default function BlogPostPage({ params }) {
    return <h1>مقال: {params.slug}</h1>;
  }
  
  // app/dashboard/layout.tsx - تخطيط مشترك لكل صفحات لوحة التحكم
  export default function DashboardLayout({ children }) {
    return (
      <div className="dashboard-layout">
        <nav>قائمة لوحة التحكم</nav>
        <main>{children}</main>
      </div>
    );
  }

الملفات الخاصة في App Router:

  • page.js: يحدد صفحة قابلة للوصول عند مسار معين
  • layout.js: يحدد تخطيط مشترك للصفحات
  • loading.js: يعرض حالة التحميل أثناء انتظار المحتوى
  • error.js: يعرض رسالة خطأ في حالة حدوث مشكلة
  • not-found.js: يعرض عندما لا يتم العثور على صفحة
  • route.js: يحدد API endpoint

Pages Router

يعتمد Pages Router على مجلد pages/ حيث يتم تحويل كل ملف إلى مسار تلقائيًا.

// pages/index.js - الصفحة الرئيسية "/"
  export default function HomePage() {
    return <h1>الصفحة الرئيسية</h1>;
  }
  
  // pages/blog/index.js - صفحة المدونة "/blog"
  export default function BlogPage() {
    return <h1>المدونة</h1>;
  }
  
  // pages/blog/[slug].js - صفحة مقال محدد "/blog/article-1"
  export default function BlogPostPage({ query }) {
    const { slug } = query;
    return <h1>مقال: {slug}</h1>;
  }
  
  // pages/_app.js - مكون التخطيط العام لكل الصفحات
  export default function MyApp({ Component, pageProps }) {
    return (    <div className="main-layout">
    <header>الترويسة العامة</header>
    <Component {...pageProps} />
    <footer>التذييل العام</footer>
  </div>
);
}

الملفات الخاصة في Pages Router:

  • _app.js: مكون يلف كل الصفحات ويستخدم للإعدادات المشتركة
  • _document.js: يتيح تخصيص المستند HTML
  • _error.js: صفحة الخطأ المخصصة
  • 404.js: صفحة عدم العثور على المحتوى المخصصة
  • 500.js: صفحة خطأ الخادم المخصصة

Layouts

التخطيطات (Layouts) تتيح مشاركة واجهة مشتركة بين عدة صفحات، مثل الترويسة والقائمة الجانبية والتذييل.

// في App Router: app/dashboard/layout.tsx
export default function DashboardLayout({
children, // محتوى الصفحات الفرعية
}) {
return (
  <div className="dashboard-container">
    <aside className="sidebar">
      <nav>
        <ul>
          <li><Link href="/dashboard">الرئيسية</Link></li>
          <li><Link href="/dashboard/analytics">التحليلات</Link></li>
          <li><Link href="/dashboard/settings">الإعدادات</Link></li>
        </ul>
      </nav>
    </aside>
    
    <main className="content">
      {children}
    </main>
  </div>
);
}

أنواع التخطيطات:

  • التخطيط الجذري (Root Layout): يطبق على كل الصفحات (مثل app/layout.js)
  • التخطيطات المتداخلة (Nested Layouts): تخطيطات خاصة بأقسام معينة من التطبيق
  • التخطيطات المجمعة (Grouped Layouts): تطبق على مجموعة من الصفحات دون التأثير على المسار
  • تخطيطات الصفحة (Page Layouts): خاصة بصفحة واحدة فقط

في App Router، تحافظ التخطيطات على حالتها وتظل مستمرة عند التنقل بين الصفحات، مما يحسن تجربة المستخدم ويحافظ على حالة المكونات.

مكونات استرجاع البيانات

في Next.js، هناك عدة طرق لاسترجاع البيانات اعتمادًا على نوع المكون وتوقيت استرجاع البيانات.

// 1. استرجاع البيانات في مكونات الخادم (App Router)
async function ProductsPage() {
// استرجاع البيانات مباشرة من الخادم
const products = await fetch('https://api.example.com/products')
  .then(res => res.json());

return (
  <div>
    <h1>المنتجات</h1>
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  </div>
);
}

// 2. استرجاع البيانات أثناء البناء (Pages Router)
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

return {
  props: {
    posts,
  },
  // إعادة توليد الصفحة كل 10 دقائق
  revalidate: 600,
};
}

export default function Blog({ posts }) {
return (
  <div>
    <h1>المدونة</h1>
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  </div>
);
}

// 3. استرجاع البيانات في كل طلب (Pages Router)
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/dashboard');
const data = await res.json();

return {
  props: {
    data,
  },
};
}

// 4. استرجاع البيانات من جانب العميل باستخدام SWR
"use client"

import useSWR from 'swr';

const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) {
  throw new Error('حدث خطأ في استرجاع البيانات');
}
return res.json();
};

function ProfileComponent() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);

if (isLoading) return <div>جاري التحميل...</div>;
if (error) return <div>حدث خطأ: {error.message}</div>;

return (
  <div>
    <h1>الملف الشخصي</h1>
    <p>مرحبًا {data.name}!</p>
  </div>
);
}

مقارنة بين طرق استرجاع البيانات:

الطريقة متى تستخدم المميزات
مكونات الخادم (Server Components) للمحتوى الثابت أو الديناميكي الذي لا يحتاج لتفاعل مباشر بسيطة، تنفذ على الخادم، تقلل JavaScript المرسل للعميل
getStaticProps للمحتوى الذي يمكن توليده وقت البناء ويتغير قليلاً سريعة جدًا، يمكن إعادة التحقق منها (ISR)، مثالية لـ SEO
getServerSideProps للمحتوى الذي يعتمد على كل طلب (مثل معلومات المستخدم) بيانات حديثة دائمًا، آمنة للبيانات الحساسة
SWR / React Query لاسترجاع البيانات التفاعلية من جانب العميل تخزين مؤقت، إعادة تحقق تلقائية، تزامن بين التبويبات

مكونات واجهة المستخدم

هذه المكونات تمثل أجزاء شائعة الاستخدام في واجهات المستخدم يمكن إعادة استخدامها عبر التطبيق.

Button

"use client"

function Button({ 
children, 
variant = 'primary', 
size = 'medium',
onClick,
disabled
}) {
const variantClasses = {
  primary: 'bg-blue-600 hover:bg-blue-700 text-white',
  secondary: 'bg-gray-600 hover:bg-gray-700 text-white',
  danger: 'bg-red-600 hover:bg-red-700 text-white',
};

const sizeClasses = {
  small: 'px-2 py-1 text-sm',
  medium: 'px-4 py-2',
  large: 'px-6 py-3 text-lg',
};

return (
  <button
    className={`rounded font-medium ${variantClasses[variant]} ${sizeClasses[size]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
    onClick={onClick}
    disabled={disabled}
  >
    {children}
  </button>
);
}

Card

function Card({ 
title, 
children,
footer
}) {
return (
  <div className="bg-white shadow-md rounded-lg overflow-hidden">
    {title && (
      <div className="px-6 py-4 border-b">
        <h3 className="text-lg font-semibold">{title}</h3>
      </div>
    )}
    
    <div className="px-6 py-4">
      {children}
    </div>
    
    {footer && (
      <div className="px-6 py-4 bg-gray-50 border-t">
        {footer}
      </div>
    )}
  </div>
);
}

Modal

"use client"

import { useEffect, useRef } from 'react';

function Modal({ 
isOpen, 
onClose, 
title, 
children, 
footer 
}) {
const modalRef = useRef(null);

useEffect(() => {
  const handleEscape = (e) => {
    if (e.key === 'Escape') {
      onClose();
    }
  };
  
  if (isOpen) {
    document.addEventListener('keydown', handleEscape);
    document.body.style.overflow = 'hidden';
  }
  
  return () => {
    document.removeEventListener('keydown', handleEscape);
    document.body.style.overflow = 'auto';
  };
}, [isOpen, onClose]);

if (!isOpen) return null;

return (
  <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
    <div
      ref={modalRef}
      className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-hidden flex flex-col"
    >
      <div className="px-6 py-4 border-b flex justify-between items-center">
        <h3 className="text-lg font-semibold">{title}</h3>
        <button onClick={onClose} className="text-gray-500 hover:text-gray-700">
          ×
        </button>
      </div>
      
      <div className="px-6 py-4 overflow-auto">
        {children}
      </div>
      
      {footer && (
        <div className="px-6 py-4 bg-gray-50 border-t">
          {footer}
        </div>
      )}
    </div>
  </div>
);
}

Tabs

"use client"

import { useState } from 'react';

function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);

return (
  <div>
    <div className="border-b">
      <nav className="flex -mb-px">
        {tabs.map((tab, index) => (
          <button
            key={index}
            className={`px-4 py-2 font-medium ${
              activeTab === index
                ? 'border-b-2 border-blue-500 text-blue-600'
                : 'text-gray-500 hover:text-gray-700'
            }`}
            onClick={() => setActiveTab(index)}
          >
            {tab.label}
          </button>
        ))}
      </nav>
    </div>
    
    <div className="mt-4">
    {tabs[activeTab].content}
  </div>
</div>
);
}

// طريقة الاستخدام
function TabExample() {
const tabsData = [
{
  label: 'الملف الشخصي',
  content: <div>محتوى الملف الشخصي</div>
},
{
  label: 'الإعدادات',
  content: <div>محتوى الإعدادات</div>
},
{
  label: 'الإشعارات',
  content: <div>محتوى الإشعارات</div>
}
];

return <Tabs tabs={tabsData} />;
}

هذه المكونات تمثل أساسيات واجهة المستخدم التي يمكن استخدامها في أي تطبيق Next.js. من المهم تصميم هذه المكونات لتكون قابلة لإعادة الاستخدام والتخصيص لمختلف الحالات.

مكونات النماذج

مكونات النماذج تُستخدم لجمع المدخلات من المستخدم، وهي تشمل حقول النص، الاختيارات، خانات الاختيار، إلخ.

Input

"use client"

function Input({
label,
type = 'text',
id,
name,
value,
onChange,
placeholder,
error,
required
}) {
return (
<div className="mb-4">
  {label && (
    <label
      htmlFor={id}
      className="block text-sm font-medium text-gray-700 mb-1"
    >
      {label}
      {required && <span className="text-red-500">*</span>}
    </label>
  )}
  
  <input
    type={type}
    id={id}
    name={name}
    value={value}
    onChange={onChange}
    placeholder={placeholder}
    required={required}
    className={`w-full px-3 py-2 border rounded-md ${
      error ? 'border-red-500' : 'border-gray-300'
    } focus:outline-none focus:ring-1 focus:ring-blue-500`}
  />
  
  {error && (
    <p className="mt-1 text-sm text-red-500">
      {error}
    </p>
  )}
</div>
);
}

Select

"use client"

function Select({
label,
id,
name,
value,
onChange,
options,
error,
required
}) {
return (
<div className="mb-4">
  {label && (
    <label
      htmlFor={id}
      className="block text-sm font-medium text-gray-700 mb-1"
    >
      {label}
      {required && <span className="text-red-500">*</span>}
    </label>
  )}
  
  <select
    id={id}
    name={name}
    value={value}
    onChange={onChange}
    required={required}
    className={`w-full px-3 py-2 border rounded-md ${
      error ? 'border-red-500' : 'border-gray-300'
    } focus:outline-none focus:ring-1 focus:ring-blue-500`}
  >
    <option value="">-- اختر --</option>
    {options.map(option => (
      <option
        key={option.value}
        value={option.value}
      >
        {option.label}
      </option>
    ))}
  </select>
  
  {error && (
    <p className="mt-1 text-sm text-red-500">
      {error}
    </p>
  )}
</div>
);
}

Form

"use client"

import { useState } from 'react';
import Input from './Input';
import Select from './Select';

function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});

const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

const subjectOptions = [
{ value: 'general', label: 'استفسار عام' },
{ value: 'support', label: 'الدعم الفني' },
{ value: 'feedback', label: 'اقتراح' },
];

const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
  ...prev,
  [name]: value
}));

// إزالة الخطأ عند تغيير القيمة
if (errors[name]) {
  setErrors(prev => ({
    ...prev,
    [name]: undefined
  }));
}
};

const validateForm = () => {
const newErrors = {};

if (!formData.name.trim()) {
  newErrors.name = 'الاسم مطلوب';
}

if (!formData.email.trim()) {
  newErrors.email = 'البريد الإلكتروني مطلوب';
} else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
  newErrors.email = 'البريد الإلكتروني غير صالح';
}

if (!formData.subject) {
  newErrors.subject = 'الموضوع مطلوب';
}

if (!formData.message.trim()) {
  newErrors.message = 'الرسالة مطلوبة';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async (e) => {
e.preventDefault();

if (!validateForm()) {
  return;
}

setIsSubmitting(true);

try {
  // محاكاة إرسال النموذج
  await new Promise(resolve => setTimeout(resolve, 1500));
  
  // في حالة التطبيق الحقيقي، سيتم إرسال البيانات هنا
  // const response = await fetch('/api/contact', {
  //   method: 'POST',
  //   headers: { 'Content-Type': 'application/json' },
  //   body: JSON.stringify(formData)
  // });
  
  setFormData({
    name: '',
    email: '',
    subject: '',
    message: ''
  });
  
  setIsSuccess(true);
  setTimeout(() => setIsSuccess(false), 5000);
} catch (error) {
  console.error('خطأ في إرسال النموذج:', error);
  setErrors({ form: 'حدث خطأ أثناء إرسال النموذج. يرجى المحاولة مرة أخرى.' });
} finally {
  setIsSubmitting(false);
}
};

return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto">
  {errors.form && (
    <div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md">
      {errors.form}
    </div>
  )}
  
  {isSuccess && (
    <div className="mb-4 p-3 bg-green-100 text-green-700 rounded-md">
      تم إرسال النموذج بنجاح!
    </div>
  )}
  
  <Input
    label="الاسم"
    id="name"
    name="name"
    value={formData.name}
    onChange={handleChange}
    error={errors.name}
    required
  />
  
  <Input
    label="البريد الإلكتروني"
    type="email"
    id="email"
    name="email"
    value={formData.email}
    onChange={handleChange}
    error={errors.email}
    required
  />
  
  <Select
    label="الموضوع"
    id="subject"
    name="subject"
    value={formData.subject}
    onChange={handleChange}
    options={subjectOptions}
    error={errors.subject}
    required
  />
  
  <div className="mb-4">
    <label
      htmlFor="message"
      className="block text-sm font-medium text-gray-700 mb-1"
    >
      الرسالة
      <span className="text-red-500">*</span>
    </label>
    
    <textarea
      id="message"
      name="message"
      value={formData.message}
      onChange={handleChange}
      rows={4}
      className={`w-full px-3 py-2 border rounded-md ${
        errors.message ? 'border-red-500' : 'border-gray-300'
      } focus:outline-none focus:ring-1 focus:ring-blue-500`}
      required
    ></textarea>
    
    {errors.message && (
      <p className="mt-1 text-sm text-red-500">
        {errors.message}
      </p>
    )}
  </div>
  
  <button
    type="submit"
    disabled={isSubmitting}
    className="w-full px-4 py-2 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
  >
    {isSubmitting ? 'جاري الإرسال...' : 'إرسال'}
  </button>
</form>
);
}

هذه المكونات تقدم طريقة متناسقة للتعامل مع مدخلات المستخدم في تطبيقات React وNext.js. يمكن تخصيصها وتوسيعها حسب احتياجات التطبيق المحددة.

ملاحظة حول التعامل مع النماذج

للتعامل مع النماذج في React وNext.js، هناك عدة طرق:

  • النماذج غير المُدارة (Uncontrolled forms): باستخدام useRef للوصول إلى قيم الحقول
  • النماذج المُدارة (Controlled forms): باستخدام useState لتخزين ومزامنة قيم الحقول
  • مكتبات النماذج: مثل Formik أو React Hook Form لإدارة حالة وتحقق النماذج المعقدة

في المثال أعلاه، استخدمنا النهج المُدار مع التحقق يدويًا. للتطبيقات الأكبر والأكثر تعقيدًا، ينصح باستخدام مكتبات متخصصة.

الخلاصة

في هذا الدليل، استعرضنا المكونات الأساسية في React وNext.js، وكيفية استخدامها لبناء تطبيقات ويب حديثة ومتطورة.

تعلمنا:

  • استخدام المكونات الوظيفية ومكونات الفئة في React
  • العمل مع React Hooks مثل useState وuseEffect وغيرها
  • إنشاء React Hooks مخصصة لاستخراج منطق الحالة المشترك
  • استخدام مكونات Next.js المدمجة مثل Link وImage
  • التعامل مع نظام التوجيه في Next.js
  • استرجاع البيانات بطرق مختلفة
  • بناء مكونات واجهة المستخدم ومكونات النماذج

بناء تطبيق React وNext.js ناجح يعتمد على استخدام المكونات المناسبة للحالات المناسبة، والتفكير في إمكانية إعادة الاستخدام والصيانة. استمر في استكشاف هذه المكونات وتجربتها في مشاريعك الخاصة لتطوير مهاراتك.