إطار Next.js

إدارة الحالة في Next.js

25 دقيقة الدرس 16 من 40

مقدمة إلى إدارة الحالة

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

مفهوم أساسي: في Next.js 13+، يجب عليك التمييز بين حالة الخادم (البيانات من قواعد البيانات، APIs) وحالة العميل (حالة واجهة المستخدم، مدخلات النماذج، تفاعلات المستخدم).

فهم أنواع الحالة

قبل الغوص في الحلول، دعنا نفهم الأنواع المختلفة من الحالة في تطبيقات 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>
  )
}
أفضل ممارسة: استخدم React Context للحالة البسيطة والمحلية. للحالة المعقدة مع التحديثات المتكررة، فكر في حلول محسّنة أكثر مثل Zustand أو Redux.

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>
  )
}
فوائد Zustand: لا حاجة لموفرين، حد أدنى من الكود النمطي، دعم ممتاز لـ TypeScript، استمرارية مدمجة، وإعادة عرض انتقائية للأداء الأمثل.

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 هي فهم متى تستخدم حالة الخادم مقابل حالة العميل.

خطأ شائع: لا تخزن بيانات الخادم (من قواعد البيانات/APIs) في مديري حالة العميل مثل Redux أو Zustand. استخدم مكونات الخادم وجلب البيانات على الخادم عندما يكون ذلك ممكناً.
<!-- ❌ سيء: الجلب في مكون العميل والتخزين في 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 موجود
تمرين عملي:
  1. أنشئ متجر Zustand لإدارة قائمة مهام مع وظائف الإضافة والإزالة والتبديل والتصفية
  2. قم بتنفيذ الاستمرارية باستخدام وسيط persist في Zustand
  3. أضف التصفية عبر معاملات بحث URL (مكتمل، نشط، الكل)
  4. أنشئ مكون خادم يعرض إحصائيات المهام بدون استخدام حالة العميل
  5. قم ببناء مبدل سمة باستخدام 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
)
نصيحة للأداء: استخدم دائماً دوال المحدد مع Zustand و Redux لضمان إعادة عرض المكونات فقط عندما تتغير البيانات المحددة التي تحتاجها.

الخلاصة

إدارة الحالة في Next.js تتطلب فهم التمييز بين حالة الخادم والعميل. استخدم مكونات الخادم لجلب البيانات، URL للحالة القابلة للمشاركة، واختر حل حالة العميل المناسب بناءً على احتياجات التعقيد الخاصة بك. ابدأ بشكل بسيط مع خطافات React و Context، ثم انتقل إلى Zustand أو Redux مع نمو تطبيقك.