إدارة الحالة في Next.js
مقدمة إلى إدارة الحالة
إدارة الحالة هي جانب حاسم في بناء تطبيقات Next.js معقدة. مع نمو تطبيقك، ستحتاج إلى أنماط فعالة لإدارة البيانات التي تتدفق عبر مكوناتك. تطبيقات Next.js لها اعتبارات فريدة لأنها تمزج بين مكونات الخادم والعميل.
فهم أنواع الحالة
قبل الغوص في الحلول، دعنا نفهم الأنواع المختلفة من الحالة في تطبيقات Next.js:
- حالة الخادم: البيانات المجلوبة من مصادر خارجية (قواعد البيانات، APIs). يجب جلبها في مكونات الخادم عندما يكون ذلك ممكناً.
- حالة العميل: حالة واجهة المستخدم، قيم النماذج، النوافذ المنبثقة، تفضيلات السمة - أي شيء يتطلب التفاعل.
- حالة URL: معاملات البحث، أجزاء المسار - الحالة المشفرة في عنوان URL.
- الحالة العامة: البيانات المشتركة عبر العديد من المكونات (مصادقة المستخدم، عربة التسوق).
React Context للحالة البسيطة
للتطبيقات الأبسط أو احتياجات الحالة المعزولة، يوفر React Context حلاً مدمجاً بدون تبعيات خارجية.
<!-- app/providers/theme-provider.tsx -->
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
<!-- app/layout.tsx -->
import { ThemeProvider } from './providers/theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
)
}
<!-- app/components/theme-toggle.tsx -->
'use client'
import { useTheme } from '../providers/theme-provider'
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
السمة الحالية: {theme}
</button>
)
}
Zustand - إدارة حالة خفيفة الوزن
Zustand هو حل إدارة حالة بسيط يعمل بشكل ممتاز مع Next.js. إنه بسيط وسريع ولا يتطلب موفري سياق.
# تثبيت Zustand npm install zustand
<!-- app/store/use-cart-store.ts -->
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
updateQuantity: (id: string, quantity: number) => void
clearCart: () => void
getTotalPrice: () => number
getTotalItems: () => number
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existingItem = state.items.find(i => i.id === item.id)
if (existingItem) {
return {
items: state.items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
)
}
}
return { items: [...state.items, item] }
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] }),
getTotalPrice: () => {
const { items } = get()
return items.reduce((total, item) =>
total + (item.price * item.quantity), 0
)
},
getTotalItems: () => {
const { items } = get()
return items.reduce((total, item) => total + item.quantity, 0)
}
}),
{
name: 'cart-storage', // مفتاح localStorage
}
)
)
<!-- app/components/cart-button.tsx -->
'use client'
import { useCartStore } from '../store/use-cart-store'
export function CartButton() {
const totalItems = useCartStore(state => state.getTotalItems())
const totalPrice = useCartStore(state => state.getTotalPrice())
return (
<button>
السلة ({totalItems}) - ${totalPrice.toFixed(2)}
</button>
)
}
<!-- app/components/add-to-cart.tsx -->
'use client'
import { useCartStore } from '../store/use-cart-store'
interface AddToCartProps {
product: {
id: string
name: string
price: number
}
}
export function AddToCart({ product }: AddToCartProps) {
const addItem = useCartStore(state => state.addItem)
const handleAddToCart = () => {
addItem({
...product,
quantity: 1
})
}
return (
<button onClick={handleAddToCart}>
أضف إلى السلة - ${product.price}
</button>
)
}
Redux Toolkit للتطبيقات المعقدة
للتطبيقات واسعة النطاق مع منطق حالة معقد، توفر Redux Toolkit ميزات قوية بما في ذلك تصحيح السفر عبر الزمن والوسيطة وأدوات DevTools الممتازة.
# تثبيت Redux Toolkit و React-Redux npm install @reduxjs/toolkit react-redux
<!-- app/store/store.ts -->
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './features/user-slice'
import productsReducer from './features/products-slice'
export const store = configureStore({
reducer: {
user: userReducer,
products: productsReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
<!-- app/store/features/user-slice.ts -->
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface UserState {
id: string | null
email: string | null
name: string | null
isAuthenticated: boolean
}
const initialState: UserState = {
id: null,
email: null,
name: null,
isAuthenticated: false,
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action: PayloadAction<Omit<UserState, 'isAuthenticated'>>) => {
state.id = action.payload.id
state.email = action.payload.email
state.name = action.payload.name
state.isAuthenticated = true
},
clearUser: (state) => {
state.id = null
state.email = null
state.name = null
state.isAuthenticated = false
},
updateUserName: (state, action: PayloadAction<string>) => {
state.name = action.payload
}
},
})
export const { setUser, clearUser, updateUserName } = userSlice.actions
export default userSlice.reducer
<!-- app/store/hooks.ts -->
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
<!-- app/providers/redux-provider.tsx -->
'use client'
import { Provider } from 'react-redux'
import { store } from '../store/store'
export function ReduxProvider({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>
}
<!-- app/layout.tsx -->
import { ReduxProvider } from './providers/redux-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<ReduxProvider>
{children}
</ReduxProvider>
</body>
</html>
)
}
<!-- app/components/user-profile.tsx -->
'use client'
import { useAppSelector, useAppDispatch } from '../store/hooks'
import { updateUserName, clearUser } from '../store/features/user-slice'
export function UserProfile() {
const user = useAppSelector(state => state.user)
const dispatch = useAppDispatch()
const handleLogout = () => {
dispatch(clearUser())
}
const handleUpdateName = (newName: string) => {
dispatch(updateUserName(newName))
}
if (!user.isAuthenticated) {
return <div>غير مسجل الدخول</div>
}
return (
<div>
<h2>مرحباً، {user.name}</h2>
<p>البريد الإلكتروني: {user.email}</p>
<button onClick={handleLogout}>تسجيل الخروج</button>
</div>
)
}
حالة الخادم مقابل حالة العميل
واحدة من أهم التمييزات في Next.js هي فهم متى تستخدم حالة الخادم مقابل حالة العميل.
<!-- ❌ سيء: الجلب في مكون العميل والتخزين في Zustand -->
'use client'
import { useEffect } from 'react'
import { useProductStore } from '../store/product-store'
export function Products() {
const { products, setProducts } = useProductStore()
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data))
}, [])
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
<!-- ✅ جيد: الجلب في مكون الخادم -->
import { prisma } from '@/lib/prisma'
export default async function Products() {
const products = await prisma.product.findMany()
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
URL كحالة
يشجع Next.js على استخدام URL للحالة التي يجب أن تكون قابلة للمشاركة والإضافة إلى الإشارات المرجعية.
<!-- app/products/page.tsx -->
import { Suspense } from 'react'
import ProductList from './product-list'
interface SearchParams {
category?: string
sort?: string
page?: string
}
export default function ProductsPage({
searchParams
}: {
searchParams: SearchParams
}) {
return (
<div>
<h1>المنتجات</h1>
<Suspense fallback={<div>جاري التحميل...</div>}>
<ProductList searchParams={searchParams} />
</Suspense>
</div>
)
}
<!-- app/products/product-filters.tsx -->
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
export function ProductFilters() {
const router = useRouter()
const searchParams = useSearchParams()
const updateFilter = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set(key, value)
router.push(`/products?${params.toString()}`)
}
return (
<div>
<select
value={searchParams.get('category') || ''}
onChange={(e) => updateFilter('category', e.target.value)}
>
<option value="">جميع الفئات</option>
<option value="electronics">إلكترونيات</option>
<option value="clothing">ملابس</option>
</select>
<select
value={searchParams.get('sort') || 'name'}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">الاسم</option>
<option value="price-asc">السعر: من الأقل للأعلى</option>
<option value="price-desc">السعر: من الأعلى للأقل</option>
</select>
</div>
)
}
اختيار حل الحالة المناسب
إليك مصفوفة قرار لمساعدتك في اختيار نهج إدارة الحالة المناسب:
- حالة URL: الفلاتر، الترقيم، مصطلحات البحث، التبويبات - أي شيء يجب أن يكون قابلاً للمشاركة
- مكونات الخادم: البيانات من قواعد البيانات/APIs التي لا تحتاج إلى تحديثات متكررة
- useState/useReducer: حالة المكون المحلي (مدخلات النماذج، حالات التبديل)
- React Context: الحالة المشتركة البسيطة (السمة، اللغة، تفضيلات المستخدم البسيطة)
- Zustand: الحالة المشتركة متوسطة التعقيد (عربة التسوق، تفضيلات واجهة المستخدم، الإشعارات)
- Redux Toolkit: الحالة المعقدة مع منطق معقد، الحاجة إلى تصحيح السفر عبر الزمن، أو نظام Redux موجود
- أنشئ متجر Zustand لإدارة قائمة مهام مع وظائف الإضافة والإزالة والتبديل والتصفية
- قم بتنفيذ الاستمرارية باستخدام وسيط persist في Zustand
- أضف التصفية عبر معاملات بحث URL (مكتمل، نشط، الكل)
- أنشئ مكون خادم يعرض إحصائيات المهام بدون استخدام حالة العميل
- قم ببناء مبدل سمة باستخدام React Context يستمر في localStorage
تحسين الأداء
عند العمل مع إدارة الحالة، ضع دائماً في الاعتبار الآثار المترتبة على الأداء:
<!-- تحسين محددات Zustand -->
// ❌ سيء: إعادة عرض عند أي تغيير في المتجر
const store = useCartStore()
// ✅ جيد: إعادة العرض فقط عندما يتغير totalItems
const totalItems = useCartStore(state => state.getTotalItems())
// ✅ أفضل: استخدم المساواة الضحلة للكائنات
import { shallow } from 'zustand/shallow'
const { items, addItem } = useCartStore(
state => ({ items: state.items, addItem: state.addItem }),
shallow
)
الخلاصة
إدارة الحالة في Next.js تتطلب فهم التمييز بين حالة الخادم والعميل. استخدم مكونات الخادم لجلب البيانات، URL للحالة القابلة للمشاركة، واختر حل حالة العميل المناسب بناءً على احتياجات التعقيد الخاصة بك. ابدأ بشكل بسيط مع خطافات React و Context، ثم انتقل إلى Zustand أو Redux مع نمو تطبيقك.