الأنماط المتقدمة
مقدمة إلى أنماط React المتقدمة في Next.js
تساعدك أنماط React المتقدمة على كتابة كود أكثر قابلية للصيانة والإعادة استخدام والأداء. في سياق Next.js، تحتاج هذه الأنماط إلى التكيف مع Server Components و Client Components، مما يخلق تحديات وفرص فريدة.
أنماط التكوين
التكوين هو أساس تطوير React. في Next.js، يصبح التكوين أكثر قوة عندما تفهم كيفية مزج Server Components و Client Components بفعالية.
أساسيات تكوين المكونات
// components/Card.tsx (Server Component)
import { ReactNode } from 'react';
interface CardProps {
header?: ReactNode;
children: ReactNode;
footer?: ReactNode;
}
export function Card({ header, children, footer }: CardProps) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// app/products/[id]/page.tsx
import { Card } from '@/components/Card';
import { AddToCartButton } from '@/components/AddToCartButton';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<Card
header={
<div>
<h1>{product.name}</h1>
<span>${product.price}</span>
</div>
}
footer={<AddToCartButton productId={product.id} />}
>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
</Card>
);
}
المبدأ الأساسي: مرر Server Components كخصائص إلى Client Components باستخدام نمط children. هذا يسمح لـ Server Components بالبقاء على الخادم أثناء تكوينها داخل Client Components.
نمط المكونات المركبة
// components/Accordion/Accordion.tsx
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
interface AccordionContextType {
activeId: string | null;
setActiveId: (id: string | null) => void;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
export function Accordion({ children }: { children: ReactNode }) {
const [activeId, setActiveId] = useState<string | null>(null);
return (
<AccordionContext.Provider value={{ activeId, setActiveId }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('يجب استخدام مكونات Accordion المركبة داخل Accordion');
}
return context;
}
export function AccordionItem({
id,
children
}: {
id: string;
children: ReactNode;
}) {
return <div className="accordion-item">{children}</div>;
}
export function AccordionHeader({
id,
children
}: {
id: string;
children: ReactNode;
}) {
const { activeId, setActiveId } = useAccordion();
const isActive = activeId === id;
return (
<button
className={`accordion-header ${isActive ? 'active' : ''}`}
onClick={() => setActiveId(isActive ? null : id)}
>
{children}
<span>{isActive ? '−' : '+'}</span>
</button>
);
}
export function AccordionPanel({
id,
children
}: {
id: string;
children: ReactNode;
}) {
const { activeId } = useAccordion();
const isActive = activeId === id;
return isActive ? (
<div className="accordion-panel">{children}</div>
) : null;
}
// الاستخدام
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionPanel
} from '@/components/Accordion';
export default function FAQPage() {
return (
<Accordion>
<AccordionItem id="1">
<AccordionHeader id="1">ما هو Next.js؟</AccordionHeader>
<AccordionPanel id="1">
Next.js هو إطار عمل React للإنتاج...
</AccordionPanel>
</AccordionItem>
<AccordionItem id="2">
<AccordionHeader id="2">كيف يعمل SSR؟</AccordionHeader>
<AccordionPanel id="2">
العرض من جانب الخادم ينشئ HTML على الخادم...
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}
نصيحة: توفر المكونات المركبة مشاركة خصائص ضمنية من خلال السياق، مما يجعل واجهة برمجة التطبيقات أنظف وأكثر مرونة من تمرير الخصائص.
مكونات الترتيب الأعلى (HOC) في Next.js
بينما تكون HOCs أقل شيوعًا في React الحديث مع الخطافات، لا تزال لديها حالات استخدام صالحة في Next.js للمخاوف الشاملة.
HOC للمصادقة
// lib/withAuth.tsx
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
export function withAuth<P extends object>(
Component: React.ComponentType<P>
) {
return async function AuthenticatedComponent(props: P) {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/login');
}
return <Component {...props} session={session} />;
};
}
// app/dashboard/page.tsx
import { withAuth } from '@/lib/withAuth';
async function DashboardPage({ session }) {
return (
<div>
<h1>مرحبًا، {session.user.name}!</h1>
<p>هذه لوحة التحكم الخاصة بك</p>
</div>
);
}
export default withAuth(DashboardPage);
HOC لجلب البيانات
// lib/withData.tsx
export function withData<T, P extends object>(
Component: React.ComponentType<P & { data: T }>,
fetcher: () => Promise<T>
) {
return async function DataComponent(props: P) {
const data = await fetcher();
return <Component {...props} data={data} />;
};
}
// الاستخدام
import { withData } from '@/lib/withData';
function UserList({ data }: { data: User[] }) {
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export default withData(UserList, async () => {
const res = await fetch('https://api.example.com/users');
return res.json();
});
تحذير: يجب استخدام HOCs بعناية مع Client Components. تعمل بشكل أفضل مع Server Components أو عند دمجها مع الحفظ المناسب في الذاكرة.
نمط خصائص العرض
يوفر نمط خصائص العرض طريقة لمشاركة الكود بين المكونات باستخدام خاصية قيمتها دالة.
مثال على متتبع الماوس
// components/MouseTracker.tsx
'use client';
import { useState, useEffect, ReactNode } from 'react';
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
render: (position: MousePosition) => ReactNode;
}
export function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return <>{render(position)}</>;
}
// الاستخدام
import { MouseTracker } from '@/components/MouseTracker';
export default function InteractivePage() {
return (
<div>
<h1>حرك الماوس حولك</h1>
<MouseTracker
render={({ x, y }) => (
<div style={{ position: 'fixed', left: x, top: y }}>
📍 {x}, {y}
</div>
)}
/>
</div>
);
}
جلب البيانات مع خصائص العرض
// components/DataFetcher.tsx
'use client';
import { useState, useEffect, ReactNode } from 'react';
interface DataFetcherProps<T> {
url: string;
children: (data: {
data: T | null;
loading: boolean;
error: Error | null;
}) => ReactNode;
}
export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return <>{children({ data, loading, error })}</>;
}
// الاستخدام
import { DataFetcher } from '@/components/DataFetcher';
export default function PostsPage() {
return (
<DataFetcher<Post[]> url="/api/posts">
{({ data, loading, error }) => {
if (loading) return <div>جارٍ التحميل...</div>;
if (error) return <div>خطأ: {error.message}</div>;
if (!data) return null;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}}
</DataFetcher>
);
}
البديل الحديث: غالبًا ما يفضل الخطافات المخصصة على خصائص العرض في React الحديث. ومع ذلك، تظل خصائص العرض مفيدة للمكونات التي تحتاج إلى التحكم في العرض بدقة.
نمط الخطافات المخصصة
الخطافات المخصصة هي النهج الحديث لمشاركة المنطق ذي الحالة بين المكونات.
خطاف useLocalStorage
// hooks/useLocalStorage.ts
'use client';
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(value));
}
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// الاستخدام
'use client';
import { useLocalStorage } from '@/hooks/useLocalStorage';
export default function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
السمة الحالية: {theme}
</button>
);
}
خطاف useDebounce
// hooks/useDebounce.ts
'use client';
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// الاستخدام
'use client';
import { useState } from 'react';
import { useDebounce } from '@/hooks/useDebounce';
export default function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// تنفيذ البحث
console.log('البحث عن:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="بحث..."
/>
);
}
نمط الفتحة لتكوين التخطيط
// components/PageLayout.tsx
import { ReactNode } from 'react';
interface PageLayoutProps {
hero?: ReactNode;
sidebar?: ReactNode;
children: ReactNode;
footer?: ReactNode;
}
export function PageLayout({
hero,
sidebar,
children,
footer
}: PageLayoutProps) {
return (
<div className="page-layout">
{hero && <div className="hero-section">{hero}</div>}
<div className="main-container">
{sidebar && <aside className="sidebar">{sidebar}</aside>}
<main className="content">{children}</main>
</div>
{footer && <footer className="footer">{footer}</footer>}
</div>
);
}
// app/products/page.tsx
import { PageLayout } from '@/components/PageLayout';
import { ProductHero } from '@/components/ProductHero';
import { FilterSidebar } from '@/components/FilterSidebar';
export default function ProductsPage() {
return (
<PageLayout
hero={<ProductHero />}
sidebar={<FilterSidebar />}
footer={<div>© 2024 المتجر</div>}
>
<h1>منتجاتنا</h1>
{/* شبكة المنتجات */}
</PageLayout>
);
}
نصيحة: يوفر نمط الفتحة أقصى قدر من المرونة لتكوين التخطيط مع الحفاظ على المكونات مستقلة وقابلة لإعادة الاستخدام.
نمط المزود للحالة العامة
// providers/CartProvider.tsx
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: number;
}
const CartContext = createContext<CartContextType | null>(null);
export function CartProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<CartItem[]>([]);
const addItem = (item: CartItem) => {
setItems(prev => {
const existing = prev.find(i => i.id === item.id);
if (existing) {
return prev.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
);
}
return [...prev, item];
});
};
const removeItem = (id: string) => {
setItems(prev => prev.filter(item => item.id !== id));
};
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider value={{ items, addItem, removeItem, total }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('يجب استخدام useCart داخل CartProvider');
}
return context;
}
// app/layout.tsx
import { CartProvider } from '@/providers/CartProvider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<CartProvider>
{children}
</CartProvider>
</body>
</html>
);
}
تمرين: بناء منشئ نماذج مع أنماط متقدمة
أنشئ منشئ نماذج مرن يتضمن:
- استخدام المكونات المركبة لـ Form و Field و Label و Input و Error
- تنفيذ خطاف
useFormValidationمخصص - توفير نظام التحقق المستند إلى السياق
- دعم التحقق على مستوى الحقل ومستوى النموذج
- تضمين التحقق غير المتزامن المتأخر
- العمل مع Server Components و Client Components
مكافأة: أضف دعمًا للنماذج المتداخلة ومصفوفات الحقول.
أنماط الأداء
الحفظ في الذاكرة مع React.memo
// components/ExpensiveComponent.tsx
'use client';
import { memo } from 'react';
interface ExpensiveComponentProps {
data: any[];
onItemClick: (id: string) => void;
}
export const ExpensiveComponent = memo(function ExpensiveComponent({
data,
onItemClick
}: ExpensiveComponentProps) {
console.log('تم عرض ExpensiveComponent');
return (
<div>
{data.map(item => (
<div key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</div>
))}
</div>
);
});
أفضل ممارسة: ادمج الأنماط المتقدمة بشكل مدروس. لا تبالغ في هندسة الحلول—اختر أبسط نمط يحل مشكلتك بفعالية.