أساسيات React.js

أنماط التصميم في React

18 دقيقة الدرس 35 من 40

أنماط التصميم في React

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

نمط الحاوية/العرض

فصل المنطق (الحاوية) عن العرض (مكونات العرض):

// مكون العرض - يتعامل فقط مع واجهة المستخدم function UserCard({ user, onEdit, onDelete }) { return ( <div className="user-card"> <img src={user.avatar} alt={user.name} /> <h3>{user.name}</h3> <p>{user.email}</p> <div className="actions"> <button onClick={() => onEdit(user.id)}>تعديل</button> <button onClick={() => onDelete(user.id)}>حذف</button> </div> </div> ); } // مكون الحاوية - يتعامل مع المنطق والحالة function UserCardContainer({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetchUser(userId).then((data) => { setUser(data); setLoading(false); }); }, [userId]); const handleEdit = (id) => { // منطق التعديل console.log('تعديل المستخدم', id); }; const handleDelete = (id) => { // منطق الحذف console.log('حذف المستخدم', id); }; if (loading) return <div>جاري التحميل...</div>; if (!user) return <div>المستخدم غير موجود</div>; return <UserCard user={user} onEdit={handleEdit} onDelete={handleDelete} />; }
ملاحظة: مع hooks React الحديثة، هذا النمط أقل شيوعًا. غالبًا ما تحل الـ hooks المخصصة محل مكونات الحاوية، ولكن مبدأ فصل المخاوف يظل قيمًا.

نمط المكونات المركبة

أنشئ مكونات تعمل معًا مع مشاركة الحالة الضمنية:

import { createContext, useContext, useState } from 'react'; // السياق للحالة المشتركة const TabsContext = createContext(); function Tabs({ children, defaultValue }) { const [activeTab, setActiveTab] = useState(defaultValue); return ( <TabsContext.Provider value={{ activeTab, setActiveTab }}> <div className="tabs">{children}</div> </TabsContext.Provider> ); } function TabList({ children }) { return <div className="tab-list" role="tablist">{children}</div>; } function Tab({ value, children }) { const { activeTab, setActiveTab } = useContext(TabsContext); const isActive = activeTab === value; return ( <button className={`tab ${isActive ? 'active' : ''}`} onClick={() => setActiveTab(value)} role="tab" aria-selected={isActive} > {children} </button> ); } function TabPanels({ children }) { return <div className="tab-panels">{children}</div>; } function TabPanel({ value, children }) { const { activeTab } = useContext(TabsContext); if (activeTab !== value) return null; return ( <div className="tab-panel" role="tabpanel"> {children} </div> ); } // إرفاق المكونات الفرعية بالمكون الرئيسي Tabs.List = TabList; Tabs.Tab = Tab; Tabs.Panels = TabPanels; Tabs.Panel = TabPanel; // الاستخدام function App() { return ( <Tabs defaultValue="profile"> <Tabs.List> <Tabs.Tab value="profile">الملف الشخصي</Tabs.Tab> <Tabs.Tab value="settings">الإعدادات</Tabs.Tab> <Tabs.Tab value="notifications">الإشعارات</Tabs.Tab> </Tabs.List> <Tabs.Panels> <Tabs.Panel value="profile"> <h2>محتوى الملف الشخصي</h2> </Tabs.Panel> <Tabs.Panel value="settings"> <h2>محتوى الإعدادات</h2> </Tabs.Panel> <Tabs.Panel value="notifications"> <h2>محتوى الإشعارات</h2> </Tabs.Panel> </Tabs.Panels> </Tabs> ); }
نصيحة: المكونات المركبة توفر واجهة برمجة مرنة وتصريحية. المكتبات مثل Radix UI و Headless UI تستخدم هذا النمط على نطاق واسع.

نمط Render Props

مشاركة الكود بين المكونات باستخدام خاصية قيمتها دالة:

import { useState, useEffect } from 'react'; // مكون يوفر موضع الماوس function MouseTracker({ render }) { const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }); }; window.addEventListener('mousemove', handleMouseMove); return () => { window.removeEventListener('mousemove', handleMouseMove); }; }, []); return render(position); } // الاستخدام مع دوال render مختلفة function App() { return ( <div> <h1>أمثلة تتبع الماوس</h1> {/* عرض كنص */} <MouseTracker render={(position) => ( <p> موضع الماوس: {position.x}, {position.y} </p> )} /> {/* عرض كعنصر متابع */} <MouseTracker render={(position) => ( <div style={{ position: 'fixed', left: position.x, top: position.y, width: 20, height: 20, borderRadius: '50%', background: 'blue', pointerEvents: 'none', transform: 'translate(-50%, -50%)', }} /> )} /> </div> ); }
تحذير: يمكن أن تؤدي render props إلى "جحيم الاستدعاء" مع المكونات المتداخلة. غالبًا ما تكون الـ hooks المخصصة بديلاً أنظف لمشاركة المنطق ذي الحالة.

المكونات ذات الترتيب الأعلى (HOC)

دالة تأخذ مكونًا وتعيد مكونًا جديدًا مع خصائص أو سلوك إضافي:

import { useEffect, useState } from 'react'; // HOC الذي يضيف فحص المصادقة function withAuth(Component) { return function AuthenticatedComponent(props) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); useEffect(() => { // فحص المصادقة checkAuth().then((authenticated) => { setIsAuthenticated(authenticated); setLoading(false); }); }, []); if (loading) return <div>جاري التحميل...</div>; if (!isAuthenticated) return <div>الرجاء تسجيل الدخول</div>; return <Component {...props} />; }; } // HOC الذي يضيف حالة التحميل function withLoading(Component) { return function LoadingComponent({ isLoading, ...props }) { if (isLoading) { return <div className="spinner">جاري التحميل...</div>; } return <Component {...props} />; }; } // المكون الأصلي function Dashboard({ data }) { return ( <div> <h1>لوحة التحكم</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); } // تطبيق HOCs متعددة const EnhancedDashboard = withAuth(withData(Dashboard, '/api/dashboard'));

نمط Hooks المخصصة

استخرج وأعد استخدام المنطق ذي الحالة باستخدام hooks مخصصة:

import { useState, useEffect, useCallback } from 'react'; // Hook مخصص للتخزين المحلي function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } }); const setValue = useCallback( (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }, [key, storedValue] ); return [storedValue, setValue]; } // Hook مخصص للتبديل function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue((v) => !v); }, []); return [value, toggle]; } // Hook مخصص لحجم النافذة function useWindowSize() { const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight, }); useEffect(() => { const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return windowSize; } // الاستخدام function App() { const [theme, setTheme] = useLocalStorage('theme', 'light'); const [isOpen, toggleOpen] = useToggle(false); const { width, height } = useWindowSize(); return ( <div className={theme}> <h1>حجم النافذة: {width} x {height}</h1> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> تبديل السمة </button> <button onClick={toggleOpen}> {isOpen ? 'إغلاق' : 'فتح'} القائمة </button> </div> ); }
نصيحة: الـ hooks المخصصة هي الطريقة الأكثر شيوعًا لمشاركة المنطق في React الحديثة. إنها أكثر مرونة من HOCs وأنظف من render props.

نمط الموفر

استخدم React Context لتوفير حالة عامة بدون prop drilling:

import { createContext, useContext, useState } from 'react'; // إنشاء سياق السمة const ThemeContext = createContext(); // مكون موفر السمة function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === '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 used within ThemeProvider'); } return context; } // التطبيق مع الموفر function App() { return ( <ThemeProvider> <Header /> <Content /> </ThemeProvider> ); }

التركيب مقابل الوراثة

توصي React بالتركيب بدلاً من الوراثة لإعادة استخدام الكود:

// سيء - استخدام الوراثة (غير موصى به في React) class BaseButton extends React.Component { render() { return <button className="btn">{this.props.children}</button>; } } // جيد - استخدام التركيب function Button({ variant = 'default', children, ...props }) { const className = `btn btn-${variant}`; return ( <button className={className} {...props}> {children} </button> ); } // الاستخدام function App() { return ( <div> <Button variant="default">افتراضي</Button> <Button variant="primary">أساسي</Button> <Button variant="danger">خطر</Button> </div> ); }
ملاحظة: التركيب أكثر مرونة من الوراثة. يمكنك دمج مكونات بسيطة لإنشاء واجهات مستخدم معقدة دون بناء تسلسلات هرمية عميقة للفئات.
تمرين 1: أنشئ مكونًا مركبًا لقائمة منسدلة مع Dropdown و Dropdown.Trigger و Dropdown.Menu و Dropdown.Item كمكونات فرعية. استخدم Context لمشاركة الحالة بين المكونات.
تمرين 2: قم ببناء ثلاثة hooks مخصصة: useAsync لمعالجة العمليات غير المتزامنة، useForm لإدارة حالة النموذج، و usePagination لمنطق الترقيم. استخدمها معًا في مكون يجلب ويعرض بيانات مرقمة.
تمرين 3: نفذ نظام علامات الميزات باستخدام نمط الموفر. أنشئ FeatureFlagProvider الذي يحمل العلامات من API و useFeature hook الذي يتحقق مما إذا كانت الميزة مُمَكَّنة. أظهر/أخفِ الميزات بناءً على العلامات.