شارك المقالة

مقدمة حول Zustand في React.js

تُعد إدارة الحالة State Management من أهم المفاهيم في تطبيقات React.js، خصوصًا عندما يكبر التطبيق وتبدأ البيانات بالانتقال بين أكثر من Component. هنا يظهر دور مكتبات مثل Redux و Context API، لكن أحيانًا تكون هذه الحلول أكبر من الحاجة الفعلية للمشروع.

مكتبة Zustand تقدم حلًا بسيطًا وسريعًا لإدارة الحالة في React، فهي تعتمد على Hooks، ولا تحتاج إلى Provider، ولا تتطلب كتابة الكثير من الأكواد أو إنشاء Reducers و Actions كما في Redux. بحسب التوثيق الرسمي، Zustand هي مكتبة صغيرة وسريعة وقابلة للتوسع لإدارة الحالة، وتعتمد على API بسيط مبني على Hooks.

في هذا المقال سنتعرف على Zustand في React.js، ثم سنقوم ببناء تطبيق Todo عملي يمكنك من خلاله إضافة المهام، حذفها، وتحديدها كمكتملة.

المصادر: Zustand Official DocsZustand on npm

ما هي مكتبة Zustand؟

Zustand هي مكتبة لإدارة الحالة في تطبيقات React، تساعدك على إنشاء Store مركزي يحتوي على البيانات والدوال المسؤولة عن تعديل هذه البيانات. الميزة الأساسية في Zustand أنها بسيطة جدًا، حيث يمكنك إنشاء Store باستخدام دالة create، ثم استخدامه مباشرة داخل أي Component.

آخر إصدار منشور على npm وقت كتابة هذا المقال هو 5.0.13، ويتم تثبيتها بالأمر npm i zustand. كما أن التوثيق الرسمي يوضح أن Zustand لا تحتاج إلى Provider، ويمكن استهلاك الحالة مباشرة باستخدام Hook يتم إنشاؤه من الـ Store.

لماذا نستخدم Zustand بدلًا من Redux أو Context؟

تتميز Zustand بعدة نقاط تجعلها مناسبة جدًا للمشاريع الصغيرة والمتوسطة، وحتى بعض المشاريع الكبيرة عند استخدامها بشكل منظم.

1. لا تحتاج إلى Provider

في Context API غالبًا تحتاج إلى تغليف التطبيق بـ Provider، أما Zustand فيمكنك إنشاء Store واستخدامه مباشرة داخل Components بدون أي تغليف إضافي.

2. كود أقل وأسهل قراءة

لا تحتاج إلى Actions منفصلة أو Reducers أو ملفات كثيرة. يمكنك وضع الحالة والدوال التي تعدلها داخل Store واحد بسيط.

3. أداء جيد مع Selectors

يمكنك اختيار جزء معين فقط من الحالة داخل Component، وبالتالي لا يتم إعادة render للـ Component إلا عند تغير هذا الجزء.

4. مناسبة للتطبيقات العملية

يمكن استخدامها لإدارة بيانات المستخدم، السلة في المتاجر الإلكترونية، إعدادات التطبيق، النوافذ المنبثقة، القوائم، أو تطبيقات Todo وغيرها.

تثبيت Zustand في React

للبدء، قم بإنشاء مشروع React باستخدام Vite:

npm create vite@latest zustand-todo-app
cd zustand-todo-app
npm install

بعد ذلك قم بتثبيت Zustand:

npm install zustand

فكرة تطبيق Todo

سنقوم بإنشاء تطبيق Todo بسيط يحتوي على المميزات التالية:

  • إضافة مهمة جديدة.
  • عرض جميع المهام.
  • تحديد المهمة كمكتملة أو غير مكتملة.
  • حذف مهمة من القائمة.
  • إظهار عدد المهام الكلي وعدد المهام المكتملة.

إنشاء Store باستخدام Zustand

داخل مجلد src قم بإنشاء مجلد جديد باسم store، ثم أنشئ ملفًا باسم todoStore.js.

import { create } from "zustand";

export const useTodoStore = create((set) => ({
  todos: [],

  addTodo: (title) =>
    set((state) => ({
      todos: [
        ...state.todos,
        {
          id: Date.now(),
          title,
          completed: false,
        },
      ],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      ),
    })),

  deleteTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

شرح الكود

قمنا أولًا باستيراد دالة create من مكتبة Zustand، ثم أنشأنا Store باسم useTodoStore. هذا الـ Store يحتوي على:

  • todos: مصفوفة تحتوي على جميع المهام.
  • addTodo: دالة لإضافة مهمة جديدة.
  • toggleTodo: دالة لتغيير حالة المهمة من غير مكتملة إلى مكتملة والعكس.
  • deleteTodo: دالة لحذف مهمة معينة.

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

إنشاء Component لإضافة المهام

داخل مجلد src أنشئ مجلدًا باسم components، ثم أنشئ ملفًا باسم TodoForm.jsx.

import { useState } from "react";
import { useTodoStore } from "../store/todoStore";

function TodoForm() {
  const [title, setTitle] = useState("");
  const addTodo = useTodoStore((state) => state.addTodo);

  const handleSubmit = (e) => {
    e.preventDefault();

    if (!title.trim()) return;

    addTodo(title);
    setTitle("");
  };

  return (
    <form onSubmit={handleSubmit} className="todo-form">
      <input
        type="text"
        placeholder="أضف مهمة جديدة..."
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />

      <button type="submit">إضافة</button>
    </form>
  );
}

export default TodoForm;

شرح الكود

في هذا الجزء استخدمنا useState لتخزين النص الذي يكتبه المستخدم في حقل الإدخال. ثم استخدمنا useTodoStore للحصول على دالة addTodo من Zustand Store.

عند إرسال النموذج، يتم التأكد أولًا أن المستخدم لم يترك الحقل فارغًا، ثم تتم إضافة المهمة ومسح حقل الإدخال.

إنشاء Component لعرض المهام

الآن أنشئ ملفًا جديدًا باسم TodoList.jsx داخل مجلد components.

import { useTodoStore } from "../store/todoStore";

function TodoList() {
  const todos = useTodoStore((state) => state.todos);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);
  const deleteTodo = useTodoStore((state) => state.deleteTodo);

  if (todos.length === 0) {
    return <p className="empty-message">لا توجد مهام حتى الآن.</p>;
  }

  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <li
          key={todo.id}
          className={`todo-item ${todo.completed ? "completed" : ""}`}
        >
          <span onClick={() => toggleTodo(todo.id)}>
            {todo.title}
          </span>

          <button onClick={() => deleteTodo(todo.id)}>
            حذف
          </button>
        </li>
      ))}
    </ul>
  );
}

export default TodoList;

شرح الكود

في هذا الـ Component قمنا بجلب:

  • قائمة المهام todos.
  • دالة تغيير حالة المهمة toggleTodo.
  • دالة حذف المهمة deleteTodo.

إذا كانت القائمة فارغة، نعرض رسالة للمستخدم. أما إذا كانت تحتوي على مهام، فنقوم بعرضها باستخدام map. عند الضغط على نص المهمة يتم تغيير حالتها، وعند الضغط على زر الحذف يتم حذفها من القائمة.

إنشاء Component للإحصائيات

لإظهار عدد المهام وعدد المهام المكتملة، أنشئ ملفًا باسم TodoStats.jsx.

import { useTodoStore } from "../store/todoStore";

function TodoStats() {
  const todos = useTodoStore((state) => state.todos);

  const totalTodos = todos.length;
  const completedTodos = todos.filter((todo) => todo.completed).length;

  return (
    <div className="todo-stats">
      <p>إجمالي المهام: {totalTodos}</p>
      <p>المهام المكتملة: {completedTodos}</p>
    </div>
  );
}

export default TodoStats;

تجميع التطبيق داخل App.jsx

افتح ملف App.jsx واكتب الكود التالي:

import TodoForm from "./components/TodoForm";
import TodoList from "./components/TodoList";
import TodoStats from "./components/TodoStats";
import "./App.css";

function App() {
  return (
    <div className="app">
      <div className="todo-container">
        <h1>تطبيق المهام باستخدام Zustand</h1>

        <TodoForm />
        <TodoStats />
        <TodoList />
      </div>
    </div>
  );
}

export default App;

إضافة تنسيقات CSS للتطبيق

افتح ملف App.css وضع التنسيقات التالية:

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background: #f3f4f6;
  direction: rtl;
}

.app {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.todo-container {
  width: 100%;
  max-width: 500px;
  background: #ffffff;
  padding: 25px;
  border-radius: 16px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}

h1 {
  text-align: center;
  margin-bottom: 20px;
  color: #111827;
}

.todo-form {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-form input {
  flex: 1;
  padding: 12px;
  border: 1px solid #d1d5db;
  border-radius: 10px;
  font-size: 16px;
}

.todo-form button,
.todo-item button {
  border: none;
  cursor: pointer;
  border-radius: 10px;
  padding: 12px 16px;
  font-size: 15px;
}

.todo-form button {
  background: #2563eb;
  color: white;
}

.todo-stats {
  display: flex;
  justify-content: space-between;
  background: #eff6ff;
  padding: 12px;
  border-radius: 10px;
  margin-bottom: 20px;
  color: #1e3a8a;
  font-weight: bold;
}

.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #f9fafb;
  padding: 12px;
  border-radius: 10px;
  margin-bottom: 10px;
}

.todo-item span {
  cursor: pointer;
  flex: 1;
}

.todo-item.completed span {
  text-decoration: line-through;
  color: #6b7280;
}

.todo-item button {
  background: #ef4444;
  color: white;
  margin-right: 10px;
}

.empty-message {
  text-align: center;
  color: #6b7280;
}

تشغيل التطبيق

بعد الانتهاء من كتابة الملفات السابقة، قم بتشغيل التطبيق باستخدام الأمر:

npm run dev

الآن يمكنك فتح التطبيق في المتصفح، وستتمكن من إضافة المهام، حذفها، وتحديدها كمكتملة.

مميزات استخدام Zustand في هذا التطبيق

  • تم فصل منطق إدارة المهام داخل Store مستقل.
  • أصبح من السهل استخدام نفس الحالة داخل أكثر من Component.
  • لم نحتج إلى تمرير Props بين Components.
  • لم نستخدم Provider أو Reducer.
  • الكود أصبح أبسط وأسهل في الصيانة.

أخطاء شائعة عند استخدام Zustand

1. جلب كل الـ Store داخل Component

من الأفضل عدم كتابة:

const store = useTodoStore();

إلا عند الحاجة، لأن ذلك قد يجعل الـ Component يتأثر بأي تغيير في الـ Store. الأفضل استخدام Selector:

const todos = useTodoStore((state) => state.todos);

2. تعديل الحالة مباشرة

لا تقم بتعديل المصفوفة أو الكائن مباشرة. استخدم دائمًا طريقة Immutable مثل map و filter و Spread Operator.

3. وضع كل شيء في Store واحد ضخم

في المشاريع الكبيرة، من الأفضل تقسيم الحالة إلى Stores أو Slices حسب نوع البيانات، مثل Store للمستخدم، Store للمهام، Store للإعدادات، وهكذا.

الخلاصة

تُعد Zustand من أفضل الخيارات البسيطة لإدارة الحالة في React.js، خصوصًا عندما تريد حلًا سريعًا وخفيفًا بدون تعقيدات Redux أو كثرة استخدام Props. في هذا المقال تعلمنا كيفية تثبيت Zustand، إنشاء Store، ثم بناء تطبيق Todo كامل يدعم إضافة المهام، حذفها، وتحديدها كمكتملة.

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


لمزيد من المقالات : شرح Bounce Animation في Flutter: إنشاء تأثير الظهور من الأسفل باحترافية
شاهد أيضًا
مقالات ذات صلة

🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع