Zustand + Next.js App Router: إدارة حالة React الحديثة من الصفر إلى الإنتاج

Noqta TeamAI Bot
بواسطة Noqta Team & AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

إدارة حالة خفيفة تعمل ببساطة. Zustand هو مدير الحالة البسيط المبني على الـ hooks الذي أصبح الخيار المفضل لمطوري React في 2026. في هذا الدليل، ستبني تطبيق سلة تسوق واقعي مع Next.js 15 App Router، متعلماً كل أنماط Zustand التي تحتاجها للإنتاج.

ما ستتعلمه

بنهاية هذا الدليل، ستكون قادراً على:

  • إعداد Zustand في مشروع Next.js 15 App Router مع TypeScript
  • إنشاء متاجر آمنة الأنواع مع إجراءات ومحددات وقيم محسوبة
  • استخدام الوسائط (middleware) للتسجيل والاستمرارية وتكامل أدوات التطوير
  • التعامل مع العرض من جانب الخادم والترطيب مع متاجر Zustand
  • تنفيذ شرائح المتجر (slices) لتنظيم الحالة المعقدة
  • بناء حالة مستمرة مع localStorage ومحركات تخزين مخصصة
  • تطبيق أنماط واقعية لسلة تسوق في بيئة الإنتاج

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبّت (node --version)
  • خبرة في TypeScript (الأنواع، الأنواع المعممة، الـ hooks)
  • إلمام بأساسيات Next.js App Router (التخطيطات، الصفحات، مكونات الخادم/العميل)
  • فهم أساسي لـ حالة React (useState، useContext)

لماذا Zustand في 2026؟

تطورت إدارة حالة React بشكل كبير. بينما هيمن Redux لسنوات، توجه النظام البيئي نحو حلول أبسط وأكثر سلاسة. إليك لماذا يتميز Zustand:

الميزةZustandRedux ToolkitJotaiContext API
حجم الحزمة~1 كيلوبايت~12 كيلوبايت~3 كيلوبايت0 كيلوبايت (مدمج)
الكود المتكررأدنى حدمتوسطأدنى حدعالٍ على نطاق واسع
تجربة TypeScriptممتازةجيدةجيدةيدوية
دعم SSRمدمجإعداد إضافيمدمجمدمج
أدوات التطويرعبر middlewareمدمجةإضافةلا يوجد
منحنى التعلممنخفضمتوسطمنخفضمنخفض (لا يتوسع جيداً)

يمنحك Zustand بساطة useState مع قوة متجر عام، بدون أي providers مطلوبة.


الخطوة 1: إعداد المشروع

أنشئ مشروع Next.js 15 جديد مع TypeScript:

npx create-next-app@latest zustand-cart --typescript --tailwind --app --src-dir
cd zustand-cart

ثبّت Zustand:

npm install zustand

يجب أن تبدو بنية مشروعك هكذا:

zustand-cart/
├── src/
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── cart/
│   │       └── page.tsx
│   ├── components/
│   ├── stores/
│   └── types/
├── package.json
└── tsconfig.json

أنشئ المجلدات التي سنحتاجها:

mkdir -p src/stores src/types src/components

الخطوة 2: تعريف الأنواع

ابدأ بتعريفات أنواع نظيفة. أنشئ src/types/cart.ts:

// src/types/cart.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  category: string;
}
 
export interface CartItem {
  product: Product;
  quantity: number;
}
 
export interface CartSummary {
  totalItems: number;
  totalPrice: number;
  discount: number;
  finalPrice: number;
}

الخطوة 3: إنشاء أول متجر Zustand

هنا يتألق Zustand. أنشئ src/stores/cart-store.ts:

// src/stores/cart-store.ts
import { create } from "zustand";
import type { Product, CartItem } from "@/types/cart";
 
interface CartState {
  // الحالة
  items: CartItem[];
  isOpen: boolean;
 
  // الإجراءات
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  toggleCart: () => void;
 
  // محسوب (حالة مشتقة)
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const useCartStore = create<CartState>((set, get) => ({
  // الحالة الأولية
  items: [],
  isOpen: false,
 
  // الإجراءات
  addItem: (product) =>
    set((state) => {
      const existingItem = state.items.find(
        (item) => item.product.id === product.id
      );
 
      if (existingItem) {
        return {
          items: state.items.map((item) =>
            item.product.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      }
 
      return { items: [...state.items, { product, quantity: 1 }] };
    }),
 
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((item) => item.product.id !== productId),
    })),
 
  updateQuantity: (productId, quantity) =>
    set((state) => ({
      items:
        quantity <= 0
          ? state.items.filter((item) => item.product.id !== productId)
          : state.items.map((item) =>
              item.product.id === productId ? { ...item, quantity } : item
            ),
    })),
 
  clearCart: () => set({ items: [] }),
 
  toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
 
  // القيم المحسوبة باستخدام get()
  getTotalItems: () =>
    get().items.reduce((total, item) => total + item.quantity, 0),
 
  getTotalPrice: () =>
    get().items.reduce(
      (total, item) => total + item.product.price * item.quantity,
      0
    ),
}));

لاحظ مدى نظافة هذا مقارنة بـ Redux. لا أنواع إجراءات، لا reducers، لا dispatch. مجرد دالة تُرجع حالة وإجراءات.


الخطوة 4: استخدام المتجر في المكونات

مكون بطاقة المنتج

أنشئ src/components/product-card.tsx:

// src/components/product-card.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
import type { Product } from "@/types/cart";
 
interface ProductCardProps {
  product: Product;
}
 
export function ProductCard({ product }: ProductCardProps) {
  const addItem = useCartStore((state) => state.addItem);
 
  return (
    <div className="rounded-lg border p-4 shadow-sm hover:shadow-md transition-shadow">
      <div className="aspect-square bg-gray-100 rounded-md mb-3 flex items-center justify-center">
        <span className="text-4xl">{product.image}</span>
      </div>
      <h3 className="font-semibold text-lg">{product.name}</h3>
      <p className="text-gray-500 text-sm">{product.category}</p>
      <div className="flex items-center justify-between mt-3">
        <span className="text-xl font-bold">{product.price.toFixed(2)} د.ت</span>
        <button
          onClick={() => addItem(product)}
          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
        >
          أضف للسلة
        </button>
      </div>
    </div>
  );
}

مكون شارة السلة

أنشئ src/components/cart-badge.tsx:

// src/components/cart-badge.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
 
export function CartBadge() {
  // اشترك فقط فيما تحتاجه - هذا هو مفتاح الأداء
  const totalItems = useCartStore((state) => state.getTotalItems());
  const toggleCart = useCartStore((state) => state.toggleCart);
 
  return (
    <button
      onClick={toggleCart}
      className="relative p-2 rounded-full hover:bg-gray-100"
    >
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z"
        />
      </svg>
      {totalItems > 0 && (
        <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {totalItems}
        </span>
      )}
    </button>
  );
}

نقطة مهمة: لاحظ كيف نستخدم (state) => state.getTotalItems() كمحدد. Zustand لا يعيد عرض المكون إلا عندما تتغير القيمة المحددة. هذا تحسين أداء تلقائي.


الخطوة 5: المحددات والأداء

Zustand لا يعيد عرض المكونات إلا عندما تتغير الحالة المحددة. هذا حاسم للأداء. إليك الأنماط:

سيء: تحديد المتجر بأكمله

// هذا يعيد العرض عند أي تغيير في الحالة
const store = useCartStore();

جيد: تحديد قيم محددة

// يعيد العرض فقط عندما تتغير items
const items = useCartStore((state) => state.items);
 
// يعيد العرض فقط عندما يتغير isOpen
const isOpen = useCartStore((state) => state.isOpen);

متقدم: دوال مساواة مخصصة

للقيم المشتقة التي تُرجع مراجع كائن جديدة، استخدم مقارنة shallow:

import { useShallow } from "zustand/react/shallow";
 
// يعيد العرض فقط عندما تتغير القيم الفعلية، وليس المرجع
const { items, isOpen } = useCartStore(
  useShallow((state) => ({
    items: state.items,
    isOpen: state.isOpen,
  }))
);

إنشاء محددات قابلة لإعادة الاستخدام

أنشئ src/stores/cart-selectors.ts:

// src/stores/cart-selectors.ts
import type { CartState } from "@/stores/cart-store";
 
export const selectCartItems = (state: CartState) => state.items;
export const selectIsCartOpen = (state: CartState) => state.isOpen;
export const selectCartTotal = (state: CartState) => state.getTotalPrice();
export const selectCartCount = (state: CartState) => state.getTotalItems();
 
export const selectItemById = (productId: string) => (state: CartState) =>
  state.items.find((item) => item.product.id === productId);

الاستخدام:

const items = useCartStore(selectCartItems);
const total = useCartStore(selectCartTotal);
const item = useCartStore(selectItemById("product-1"));

الخطوة 6: الوسائط - الاستمرارية

واحدة من أقوى ميزات Zustand هي نظام الوسائط (middleware). دعنا نضيف الاستمرارية لحفظ السلة في localStorage.

حدّث src/stores/cart-store.ts:

// src/stores/cart-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import type { Product, CartItem } from "@/types/cart";
 
interface CartState {
  items: CartItem[];
  isOpen: boolean;
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  toggleCart: () => void;
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      isOpen: false,
 
      addItem: (product) =>
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.product.id === product.id
          );
          if (existingItem) {
            return {
              items: state.items.map((item) =>
                item.product.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          }
          return { items: [...state.items, { product, quantity: 1 }] };
        }),
 
      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((item) => item.product.id !== productId),
        })),
 
      updateQuantity: (productId, quantity) =>
        set((state) => ({
          items:
            quantity <= 0
              ? state.items.filter((item) => item.product.id !== productId)
              : state.items.map((item) =>
                  item.product.id === productId ? { ...item, quantity } : item
                ),
        })),
 
      clearCart: () => set({ items: [] }),
      toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
      getTotalItems: () =>
        get().items.reduce((total, item) => total + item.quantity, 0),
      getTotalPrice: () =>
        get().items.reduce(
          (total, item) => total + item.product.price * item.quantity,
          0
        ),
    }),
    {
      name: "cart-storage",
      storage: createJSONStorage(() => localStorage),
      // استمرار البيانات فقط، وليس حالة الواجهة مثل isOpen
      partialize: (state) => ({ items: state.items }),
    }
  )
);

خيار partialize مهم: يجب أن تحفظ بيانات الحالة فقط، وليس حالة الواجهة مثل ما إذا كان درج السلة مفتوحاً.


الخطوة 7: التعامل مع SSR والترطيب

يقوم Next.js App Router بعرض المكونات على الخادم أولاً. بما أن localStorage غير موجود على الخادم، نحتاج للتعامل مع الترطيب بعناية.

أنشئ src/stores/hydration.ts:

// src/stores/hydration.ts
"use client";
 
import { useEffect, useState } from "react";
 
export function useHydration() {
  const [hydrated, setHydrated] = useState(false);
 
  useEffect(() => {
    setHydrated(true);
  }, []);
 
  return hydrated;
}

أنشئ مكون سلة آمن للترطيب في src/components/cart-sidebar.tsx:

// src/components/cart-sidebar.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
import { useHydration } from "@/stores/hydration";
 
export function CartSidebar() {
  const hydrated = useHydration();
  const items = useCartStore((state) => state.items);
  const isOpen = useCartStore((state) => state.isOpen);
  const toggleCart = useCartStore((state) => state.toggleCart);
  const removeItem = useCartStore((state) => state.removeItem);
  const updateQuantity = useCartStore((state) => state.updateQuantity);
  const clearCart = useCartStore((state) => state.clearCart);
  const getTotalPrice = useCartStore((state) => state.getTotalPrice);
 
  if (!isOpen) return null;
 
  return (
    <div className="fixed inset-0 z-50 flex justify-end">
      {/* خلفية داكنة */}
      <div className="absolute inset-0 bg-black/50" onClick={toggleCart} />
 
      {/* الشريط الجانبي */}
      <div className="relative w-full max-w-md bg-white shadow-xl p-6 overflow-y-auto">
        <div className="flex justify-between items-center mb-6">
          <h2 className="text-2xl font-bold">سلة التسوق</h2>
          <button onClick={toggleCart} className="text-gray-500 hover:text-gray-700">

          </button>
        </div>
 
        {!hydrated ? (
          <div className="animate-pulse space-y-4">
            {[1, 2, 3].map((i) => (
              <div key={i} className="h-20 bg-gray-200 rounded" />
            ))}
          </div>
        ) : items.length === 0 ? (
          <p className="text-gray-500 text-center py-8">سلتك فارغة</p>
        ) : (
          <>
            <div className="space-y-4">
              {items.map((item) => (
                <div
                  key={item.product.id}
                  className="flex items-center gap-4 border-b pb-4"
                >
                  <span className="text-3xl">{item.product.image}</span>
                  <div className="flex-1">
                    <h3 className="font-medium">{item.product.name}</h3>
                    <p className="text-gray-500">
                      {item.product.price.toFixed(2)} د.ت
                    </p>
                  </div>
                  <div className="flex items-center gap-2">
                    <button
                      onClick={() =>
                        updateQuantity(item.product.id, item.quantity - 1)
                      }
                      className="w-8 h-8 rounded-full border flex items-center justify-center"
                    >
                      -
                    </button>
                    <span className="w-8 text-center">{item.quantity}</span>
                    <button
                      onClick={() =>
                        updateQuantity(item.product.id, item.quantity + 1)
                      }
                      className="w-8 h-8 rounded-full border flex items-center justify-center"
                    >
                      +
                    </button>
                  </div>
                  <button
                    onClick={() => removeItem(item.product.id)}
                    className="text-red-500 hover:text-red-700"
                  >

                  </button>
                </div>
              ))}
            </div>
 
            <div className="mt-6 border-t pt-4">
              <div className="flex justify-between text-xl font-bold">
                <span>المجموع:</span>
                <span>{getTotalPrice().toFixed(2)} د.ت</span>
              </div>
              <button className="w-full mt-4 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors">
                إتمام الشراء
              </button>
              <button
                onClick={clearCart}
                className="w-full mt-2 text-gray-500 hover:text-gray-700"
              >
                تفريغ السلة
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

الخطوة 8: شرائح المتجر للتطبيقات المعقدة

مع نمو تطبيقك، ستحتاج لتقسيم متجرك إلى شرائح. هذا يبقي كل جزء مركزاً وقابلاً للاختبار.

أنشئ src/stores/slices/cart-slice.ts:

// src/stores/slices/cart-slice.ts
import type { StateCreator } from "zustand";
import type { Product, CartItem } from "@/types/cart";
 
export interface CartSlice {
  items: CartItem[];
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  clearCart: () => void;
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const createCartSlice: StateCreator<CartSlice> = (set, get) => ({
  items: [],
 
  addItem: (product) =>
    set((state) => {
      const existing = state.items.find((i) => i.product.id === product.id);
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.product.id === product.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          ),
        };
      }
      return { items: [...state.items, { product, quantity: 1 }] };
    }),
 
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((i) => i.product.id !== productId),
    })),
 
  clearCart: () => set({ items: [] }),
 
  getTotalItems: () =>
    get().items.reduce((sum, i) => sum + i.quantity, 0),
 
  getTotalPrice: () =>
    get().items.reduce((sum, i) => sum + i.product.price * i.quantity, 0),
});

أنشئ src/stores/slices/ui-slice.ts:

// src/stores/slices/ui-slice.ts
import type { StateCreator } from "zustand";
 
export interface UISlice {
  isCartOpen: boolean;
  isSearchOpen: boolean;
  theme: "light" | "dark";
  toggleCart: () => void;
  toggleSearch: () => void;
  setTheme: (theme: "light" | "dark") => void;
}
 
export const createUISlice: StateCreator<UISlice> = (set) => ({
  isCartOpen: false,
  isSearchOpen: false,
  theme: "light",
 
  toggleCart: () => set((state) => ({ isCartOpen: !state.isCartOpen })),
  toggleSearch: () => set((state) => ({ isSearchOpen: !state.isSearchOpen })),
  setTheme: (theme) => set({ theme }),
});

ادمج الشرائح في src/stores/app-store.ts:

// src/stores/app-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
 
type AppStore = CartSlice & UISlice;
 
export const useAppStore = create<AppStore>()(
  persist(
    (...args) => ({
      ...createCartSlice(...args),
      ...createUISlice(...args),
    }),
    {
      name: "app-storage",
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ items: state.items, theme: state.theme }),
    }
  )
);

الآن لديك متجر نظيف ومعياري يتوسع مع تطبيقك.


الخطوة 9: أدوات التطوير ووسائط التسجيل

اجمع عدة وسائط لتجربة تطوير ممتازة:

// src/stores/app-store.ts (نسخة محسّنة)
import { create } from "zustand";
import { persist, createJSONStorage, devtools } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
 
type AppStore = CartSlice & UISlice;
 
export const useAppStore = create<AppStore>()(
  devtools(
    persist(
      (...args) => ({
        ...createCartSlice(...args),
        ...createUISlice(...args),
      }),
      {
        name: "app-storage",
        storage: createJSONStorage(() => localStorage),
        partialize: (state) => ({ items: state.items, theme: state.theme }),
      }
    ),
    { name: "AppStore" }
  )
);

ثبّت إضافة Redux DevTools في المتصفح لفحص متجر Zustand في الوقت الفعلي. كل تغيير في الحالة سيظهر مع أسماء الإجراءات وفروقات الحالة.

وسيط تسجيل مخصص

// src/stores/middleware/logger.ts
import type { StateCreator, StoreMutatorIdentifier } from "zustand";
 
type Logger = <
  T,
  Mps extends [StoreMutatorIdentifier, unknown][] = [],
  Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
  f: StateCreator<T, Mps, Mcs>,
  name?: string
) => StateCreator<T, Mps, Mcs>;
 
type LoggerImpl = <T>(
  f: StateCreator<T, [], []>,
  name?: string
) => StateCreator<T, [], []>;
 
const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
  const loggedSet: typeof set = (...args) => {
    const prev = get();
    set(...(args as Parameters<typeof set>));
    const next = get();
    if (process.env.NODE_ENV === "development") {
      console.log(`[${name ?? "store"}]`, { prev, next });
    }
  };
  return f(loggedSet, get, store);
};
 
export const logger = loggerImpl as unknown as Logger;

الخطوة 10: تجميع كل شيء

أنشئ الصفحة الرئيسية في src/app/page.tsx:

// src/app/page.tsx
import { ProductCard } from "@/components/product-card";
import { CartBadge } from "@/components/cart-badge";
import { CartSidebar } from "@/components/cart-sidebar";
import type { Product } from "@/types/cart";
 
const products: Product[] = [
  { id: "1", name: "سماعات لاسلكية", price: 79.99, image: "🎧", category: "إلكترونيات" },
  { id: "2", name: "لوحة مفاتيح ميكانيكية", price: 149.99, image: "⌨️", category: "إلكترونيات" },
  { id: "3", name: "حذاء رياضي", price: 129.99, image: "👟", category: "رياضة" },
  { id: "4", name: "آلة صنع القهوة", price: 89.99, image: "☕", category: "مطبخ" },
  { id: "5", name: "مصباح مكتبي", price: 39.99, image: "💡", category: "مكتب" },
  { id: "6", name: "حقيبة ظهر", price: 59.99, image: "🎒", category: "إكسسوارات" },
];
 
export default function HomePage() {
  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-white shadow-sm sticky top-0 z-40">
        <div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
          <h1 className="text-2xl font-bold">متجر Zustand</h1>
          <CartBadge />
        </div>
      </header>
 
      <main className="max-w-7xl mx-auto px-4 py-8">
        <h2 className="text-3xl font-bold mb-8">المنتجات</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      </main>
 
      <CartSidebar />
    </div>
  );
}

الخطوة 11: اختبار متاجر Zustand

متاجر Zustand هي دوال بسيطة، مما يجعلها سهلة الاختبار:

// src/stores/__tests__/cart-store.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useCartStore } from "../cart-store";
 
const mockProduct = {
  id: "test-1",
  name: "منتج تجريبي",
  price: 29.99,
  image: "🧪",
  category: "اختبار",
};
 
describe("Cart Store", () => {
  beforeEach(() => {
    // إعادة تعيين المتجر بين الاختبارات
    useCartStore.setState({ items: [] });
  });
 
  it("يضيف عنصراً إلى السلة", () => {
    useCartStore.getState().addItem(mockProduct);
    const items = useCartStore.getState().items;
 
    expect(items).toHaveLength(1);
    expect(items[0].product.id).toBe("test-1");
    expect(items[0].quantity).toBe(1);
  });
 
  it("يزيد الكمية للعناصر الموجودة", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().addItem(mockProduct);
 
    const items = useCartStore.getState().items;
    expect(items).toHaveLength(1);
    expect(items[0].quantity).toBe(2);
  });
 
  it("يحذف عنصراً من السلة", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().removeItem("test-1");
 
    expect(useCartStore.getState().items).toHaveLength(0);
  });
 
  it("يحسب السعر الإجمالي بشكل صحيح", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().addItem(mockProduct);
 
    expect(useCartStore.getState().getTotalPrice()).toBe(59.98);
  });
 
  it("يفرغ السلة بالكامل", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().clearCart();
 
    expect(useCartStore.getState().items).toHaveLength(0);
  });
});

لاحظ كيف تصل إلى المتجر مباشرة باستخدام getState() و setState() خارج مكونات React. لا حاجة لأدوات العرض أو الـ providers.


استكشاف الأخطاء وإصلاحها

تحذيرات عدم تطابق الترطيب

إذا رأيت تحذيرات عدم تطابق الترطيب مع المتاجر المستمرة، تأكد من أن مكونات العميل التي تستخدم حالة مستمرة تعرض هيكل تحميل حتى اكتمال الترطيب:

const hydrated = useHydration();
if (!hydrated) return <Skeleton />;

المتجر لا يتحدث بين التبويبات

أضف مستمع حدث storage للمزامنة بين التبويبات:

persist(
  // ... إعداد المتجر
  {
    name: "cart-storage",
    storage: createJSONStorage(() => localStorage),
    // هذا يفعّل المزامنة بين التبويبات
    skipHydration: false,
  }
)

مشاكل الأداء مع المتاجر الكبيرة

استخدم محددات دقيقة بدلاً من تحديد المتجر بأكمله. استخدم useShallow عند تحديد قيم متعددة تُرجع مراجع كائن جديدة.


الخطوات التالية

الآن بعد إتقانك Zustand مع Next.js App Router، إليك بعض الاتجاهات لتطوير مهاراتك أكثر:

  • أضف وسيط Immer لتحديثات حالة متداخلة أكثر سلاسة
  • ابنِ قائمة أمنيات باستخدام شريحة متجر منفصلة
  • تكامل مع API حقيقي باستخدام إجراءات Zustand غير المتزامنة
  • أضف تحديثات متفائلة لطفرات الخادم
  • استكشف subscribeWithSelector من Zustand للتأثيرات الجانبية خارج React

الخلاصة

يقدم Zustand التوازن المثالي بين البساطة والقوة لإدارة حالة React في 2026. واجهته البرمجية البسيطة، ودعمه الممتاز لـ TypeScript، وتكامله السلس مع Next.js App Router يجعله الخيار الأمثل للتطبيقات الحديثة.

لقد تعلمت كيفية إنشاء المتاجر، واستخدام المحددات للأداء، وإضافة الاستمرارية، والتعامل مع ترطيب SSR، وتنظيم الكود بالشرائح، واختبار كل شيء. هذه الأنماط تعمل من تطبيقات العداد البسيطة إلى منصات التجارة الإلكترونية المعقدة.

النقطة الرئيسية: ابدأ ببساطة مع متجر واحد، أضف الوسائط حسب الحاجة، وقسّم إلى شرائح فقط عندما تتطلب التعقيدات ذلك. Zustand ينمو مع تطبيقك دون أن يعيقك أبداً.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على دمج ALLaM-7B-Instruct-preview مع Ollama.

ناقش مشروعك معنا

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

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

30 د قراءة·

بناء روبوت دردشة ذكاء اصطناعي محلي باستخدام Ollama و Next.js: الدليل الشامل

ابنِ روبوت دردشة ذكاء اصطناعي خاص يعمل بالكامل على جهازك المحلي باستخدام Ollama و Next.js. يغطي هذا الدليل العملي التثبيت والبث المباشر واختيار النماذج وبناء واجهة دردشة جاهزة للإنتاج — كل ذلك دون إرسال بياناتك إلى السحابة.

25 د قراءة·

بناء موقع محتوى متكامل باستخدام Payload CMS 3 و Next.js

تعلّم كيفية بناء موقع محتوى كامل الميزات باستخدام Payload CMS 3 الذي يعمل مباشرة داخل Next.js App Router. يغطي هذا الدرس المجموعات، محرر النصوص الغنية، رفع الوسائط، المصادقة، والنشر في بيئة الإنتاج.

30 د قراءة·