المكونات (Components) هي اللبنات الأساسية في تطبيقات React وNext.js. وهي وحدات قابلة لإعادة الاستخدام تمكنك من تقسيم واجهة المستخدم إلى أجزاء مستقلة ومعزولة.
قبل التعمق في المكونات المختلفة، من المهم فهم المفاهيم الأساسية:
بيانات تمرر إلى المكون من المكون الأب، وهي للقراءة فقط (read-only) ولا يمكن تغييرها داخل المكون.
بيانات خاصة بالمكون ويمكن تغييرها، وعند تغييرها يتم إعادة رندر المكون لعرض التغييرات.
دورة حياة المكون من لحظة إنشائه حتى إزالته، تتيح تنفيذ كود في مراحل معينة.
امتداد للغة JavaScript يتيح كتابة عناصر HTML داخل كود JavaScript بشكل سهل وواضح.
في الأقسام التالية، سنستعرض المكونات المختلفة في React وNext.js، وكيفية استخدامها، ومتى يجب استخدام كل منها.
React يوفر أنواعًا مختلفة من المكونات لبناء تطبيقك. لنتعرف على الأنواع الرئيسية وكيفية استخدامها.
المكونات الوظيفية هي دوال 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 };
استخدم المكونات الوظيفية في معظم الحالات، خاصة للمكونات البسيطة التي تعرض بيانات أو تستقبل تفاعلات بسيطة.
مكونات الفئة هي فئات 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;
الخاصية | المكونات الوظيفية | مكونات الفئة |
---|---|---|
الكتابة | دوال JavaScript عادية | فئات ES6 تمتد من React.Component |
إدارة الحالة | useState Hook | this.state و this.setState |
دورة الحياة | useEffect وغيرها من Hooks | طرق دورة الحياة مثل componentDidMount |
this | لا يستخدم this | يتطلب فهم this وربطه بالدوال |
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> </> ); }
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>
بالرغم من أن العنصر موجود خارج التسلسل الهرمي للـ DOM، إلا أنه يظل ضمن نفس شجرة React، مما يعني أن الأحداث ستنتشر بشكل طبيعي.
Hooks هي ميزة أضيفت في React 16.8 تتيح لك استخدام حالة ومزايا React الأخرى دون كتابة مكون فئة. الـ Hooks تسمح لك بإعادة استخدام منطق الحالة بين المكونات دون تغيير هيكلها.
يسمح للمكونات الوظيفية بإدارة الحالة المحلية.
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> ); }
يسمح بإجراء تأثيرات جانبية في المكونات الوظيفية. يمكن استخدامه لمحاكاة أساليب دورة الحياة مثل 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(() => {
// ينفذ عند تغير id أو name
}, [id, name]);
useEffect(() => { // التأثير return () => { // التنظيف }; }, [dependencies]);
يسمح بالوصول إلى القيم من 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> ); }
يسمح بالاحتفاظ بقيمة مرجعية قابلة للتغيير دون إعادة رندر المكون عند تغييرها، ويستخدم أيضًا للوصول إلى عناصر 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> ); }
يتيح تخزين نتائج العمليات الحسابية المكلفة وإعادة استخدامها فقط عند تغير القيم ذات الصلة.
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> ); }
يقوم بتخزين وإعادة استخدام تعريفات الدوال عبر عمليات الرندر، مما يساعد في تحسين الأداء لتجنب إعادة إنشاء الدوال في كل مرة.
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(() => calculation(a, b), [a, b]); |
useCallback(() => doSomething(a, b), [a, b]); |
مفيد لتخزين قيم مكلفة الحساب | مفيد عند تمرير دوال إلى مكونات فرعية محسنة |
تمكنك من استخراج منطق الحالة المشترك بين المكونات واستخدامه في مكونات متعددة. 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> ); }
يوفر Next.js مجموعة من المكونات المدمجة التي تساعد في بناء تطبيقات ويب متقدمة وتحسين الأداء وتجربة المستخدم.
مكون Link
يستخدم للتنقل بين الصفحات في تطبيق Next.js. يقوم بتسبيق تحميل الصفحات (prefetching) وتنفيذ التنقل من جانب العميل (client-side navigation).
import Link from 'next/link'; function Navigation() { return ( <nav> <ul> <li> <Link href="/">الرئيسية</Link> </li> <li> <Link href="/about">من نحن</Link> </li> <li> <Link href="/blog">المدونة</Link> </li> <li> <Link href={`/products/${productId}`} scroll={false} // إلغاء التمرير التلقائي للأعلى prefetch={false} // إلغاء التحميل المسبق > المنتج </Link> </li> </ul> </nav> ); }
مكون 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 // لتجاوز التحسين للصور الخارجية /> ); }
مكون Head
يسمح بتعديل عنوان الصفحة والـ meta tags لتحسين SEO وتخصيص كل صفحة. في الإصدارات الأحدث من Next.js App Router، تم استبدال هذا المكون باستخدام metadata API.
// في Pages Router (الطريقة القديمة) import Head from 'next/head'; function ProductPage({ product }) { return ( <> <Head> <title>{product.name} - متجرنا</title> <meta name="description" content={product.description} /> <meta property="og:title" content={`${product.name} - متجرنا`} /> <meta property="og:image" content={product.imageUrl} /> <link rel="canonical" href={`https://example.com/products/${product.id}`} /> </Head> <div> // محتوى الصفحة </div> </> ); } // في App Router (الطريقة الجديدة) import { Metadata } from 'next'; // تحديد metadata للصفحة (يتم تنفيذه على الخادم) export async function generateMetadata({ params }) { const product = await getProduct(params.id); return { title: `${product.name} - متجرنا`, description: product.description, openGraph: { title: `${product.name} - متجرنا`, images: [product.imageUrl], }, }; } export default function ProductPage({ params }) { // محتوى الصفحة }
مكون Head | Metadata API |
---|---|
مستخدم في Pages Router | مستخدم في App Router |
يعمل كمكون React | يعمل من خلال دوال خاصة أو متغيرات |
يمكن استخدامه في أي مستوى من المكونات | يتم تحديده على مستوى صفحة أو تخطيط |
يتم تنفيذه على جانب العميل والخادم | يتم تنفيذه على الخادم فقط، مما يعني أداء أفضل |
مكون 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> </> ); }
يوفر Next.js نظامين للتوجيه: App Router (الإصدار 13 وما بعده) وPages 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> ); }
يعتمد 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> ); }
التخطيطات (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> ); }
في 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 | لاسترجاع البيانات التفاعلية من جانب العميل | تخزين مؤقت، إعادة تحقق تلقائية، تزامن بين التبويبات |
هذه المكونات تمثل أجزاء شائعة الاستخدام في واجهات المستخدم يمكن إعادة استخدامها عبر التطبيق.
"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> ); }
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> ); }
"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> ); }
"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. من المهم تصميم هذه المكونات لتكون قابلة لإعادة الاستخدام والتخصيص لمختلف الحالات.
مكونات النماذج تُستخدم لجمع المدخلات من المستخدم، وهي تشمل حقول النص، الاختيارات، خانات الاختيار، إلخ.
"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> ); }
"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> ); }
"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، هناك عدة طرق:
في المثال أعلاه، استخدمنا النهج المُدار مع التحقق يدويًا. للتطبيقات الأكبر والأكثر تعقيدًا، ينصح باستخدام مكتبات متخصصة.
في هذا الدليل، استعرضنا المكونات الأساسية في React وNext.js، وكيفية استخدامها لبناء تطبيقات ويب حديثة ومتطورة.
تعلمنا:
بناء تطبيق React وNext.js ناجح يعتمد على استخدام المكونات المناسبة للحالات المناسبة، والتفكير في إمكانية إعادة الاستخدام والصيانة. استمر في استكشاف هذه المكونات وتجربتها في مشاريعك الخاصة لتطوير مهاراتك.