فلسفة إدارة الحالة
🎯 السؤال الأساسي
عندما يكبر التطبيق، كيف يمكن للمكونات مشاركة البيانات ومزامنتها بشكل أنيق؟ قد تواجه هذا المأزق: يضيف المستخدم منتجًا إلى سلة التسوق في صفحة المنتج، لكن عداد السلة في الرأس لا يتحدّث؛ مكونان غير مرتبطين يحتاجان نفس البيانات، لكنك لا تعرف كيف تمررها. سيأخذك هذا الفصل من "تمرير البيانات الفوضوي" إلى "إدارة الحالة الواضحة".
1. لماذا "المكونات وإدارة الحالة"؟
1.1 من الورشة الصغيرة إلى المصنع: تطور تطوير الواجهات الأمامية
قبل أن نبدأ رسميًا، دعني أسألك سؤالًا: هل جرّبت يومًا تحضير وجبة كبيرة في المطبخ؟
إذا كنت تطهو وعاءً من المعكرونة لنفسك فقط، فالأمر بسيط — قدرٌ واحد، قليل من المعكرونة، بعض التوابل، وتنتهي في عشر ثوانٍ. لكن إذا كنت ستفتح مطعمًا يخدم مئات الزبائن يوميًا، فلا يمكنك بعد الآن أن "تفعل ما يحلو لك". ستحتاج إلى وصفات موحدة، وتقسيم واضح للعمل، وعمليات شراء موحدة — بهذه الطريقة فقط يمكنك ضمان جودة ثابتة لكل طبق وكفاءة عالية في التحضير.
تطوير الواجهات الأمامية لا يختلف. عندما تكتب مشروعًا صغيرًا بمفردك، يمكنك وضع الكود في أي مكان. لكن عندما يكبر الفريق ويصبح المشروع أكثر تعقيدًا، ستحتاج إلى طريقة منهجية لتنظيم الكود وإدارة البيانات. هذا هو بالضبط ما تحله المكونات وإدارة الحالة.
🤔 ما هو "المكون" وما هي "الحالة"؟
قبل أن نستمر، دعنا نشرح مصطلحين أساسيين:
المكون (Component): مثل قطع الليغو، كل قطعة هي جزء مستقل له شكله ولونه ووظيفته الخاصة. يمكنك تجميع عدة قطع معًا لبناء قلعة معقدة. في تطوير الواجهات الأمامية، الزر، النموذج، شريط التنقل — كل منها يمكن أن يكون مكونًا.
الحالة (State): هي "ذاكرة" المكون. مثلًا، زر "يتذكر" ما إذا كان في حالة "معطل" أو "مفعل"؛ مكون سلة التسوق "يتذكر" المنتجات الموجودة بداخله. الحالة تتغير، وتغير الحالة يؤدي إلى تحديث الواجهة.
المكونات + إدارة الحالة = كود منظم + تدفق بيانات واضح
🏠 نمط الورشة الصغيرة
- الكود مكتوب في ملف واحد، مثل طهي كل شيء في قدر واحد
- البيانات تتنقل في كل مكان، مثل نادل يركض حاملًا الأطباق في أرجاء المطعم
- تعديل في مكان قد يؤثر على أماكن أخرى، مثل وضع الكثير من الملح فيفسد الطبق كله
🏭 نمط المصنع
- الكود مقسم إلى مكونات، مثل مطعم مقسم إلى صالة ومطبخ وقسم مشتريات
- البيانات تُدار مركزيًا، مثل وجود مستودع موحد ونظام توزيع
- تأثير التعديلات واضح النطاق، مثل تغيير طبق لا يؤثر على المطعم كله
1.2 قصة واقعية من الأخطاء: لماذا تحتاج إلى فهم إدارة الحالة
قد تقول: "ألا أستخدم Vue/React؟ أليس لديهما بالفعل إدارة حالة؟" دعني أحكِ لك قصة واقعية، وستفهم لماذا الفهم المنهجي للمكونات وإدارة الحالة مهم جدًا.
قصة أخطاء شياو مي
شياو مي كانت مديرة منتج في شركة تجارة إلكترونية وانتقلت إلى تطوير الواجهات الأمامية، وتولت للتو إعادة بناء ميزة سلة التسوق في الشركة. كانت تستخدم سابقًا مشاريع قديمة بحقبة jQuery، والآن تريد التحول إلى Vue 3.
فكرت شياو مي: "منطق سلة التسوق بسيط، فقط أخزن مصفوفة." فبدأت بكتابة الكود:
- في مكون صفحة تفاصيل المنتج، استخدمت مصفوفة
cartلتخزين بيانات السلة - في مكون صفحة سلة التسوق، عرّفت مصفوفة أخرى
cartItems - في مكون شريط التنقل العلوي، كان هناك متغير
cartCount
ظهرت المشاكل بسرعة:
- البيانات غير متزامنة: أضاف المستخدم منتجًا في صفحة التفاصيل، لكن بيانات صفحة السلة لم تتحدّث
- كود مكرر: اضطرت شياو مي لكتابة عدة دوال "إضافة إلى السلة"، موزعة على مكونات مختلفة
- صعوبة الصيانة: طلب قسم العمليات إضافة ميزة "إفراغ السلة"، فاكتشفت شياو مي أنها بحاجة لتعديل ثلاثة أماكن
لاحقًا، استشارت المهندس المعماري للواجهات الأمامية أتشيانغ، الذي نظر إلى الكود وقال فورًا: "لقد ارتكبتِ خطأً كبيرًا في إدارة الحالة — نفس البيانات مخزنة في عدة أماكن."
الحل بسيط جدًا: استخدم Pinia لإنشاء إدارة حالة عامة لسلة التسوق، بحيث تقرأ جميع المكونات وتكتب البيانات من نفس المكان. بعد هذا التعديل، حُلّت جميع المشاكل.
منذ ذلك الحين، فهمت شياو مي درسًا: بدون فهم المكونات وإدارة الحالة، ستكتب "كود معكرونة" يصعب صيانته.
💡 الدرس الأساسي
المكونات وإدارة الحالة ليست "ميزة إضافية" في الأطر، بل هي حجر الزاوية في تطوير الواجهات الأمامية الحديث. فهمها يمكنك من تصميم معمارية واضحة، وكتابة كود قابل للصيانة، والعمل بسلاسة ضمن فريق.
2. المفاهيم الأساسية: فهم جوهر المكونات
🤔 ما هو "التفكير بالمكونات"؟
التفكير بالمكونات هو طريقة لتفكيك الواجهات المعقدة إلى وحدات كود مستقلة، قابلة لإعادة الاستخدام، وذات مسؤولية واحدة.
للتشبيه: تخيل أنك تجمع حاسوبًا. ستشتري المعالج والذاكرة والقرص الصلب وبطاقة الرسوميات بشكل منفصل، ثم تجمعها معًا. كل قطعة لها وظيفة محددة، ويمكنك استبدال أي قطعة في أي وقت دون التأثير على الأجزاء الأخرى.
المكونات تجعل كود الواجهة الأمامية "معياريًا" بنفس الطريقة — كل مكون مسؤول عن شؤونه الخاصة، ويتعاون مع المكونات الأخرى عبر واجهات واضحة.
2.1 فهم المكونات من خلال تشبيه المطعم
لنستخدم تشبيه المطعم لفهم الأفكار الأساسية للمكونات:
| المفهوم | 🍽️ تشبيه المطعم | الدور الفعلي | مثال ملموس |
|---|---|---|---|
| المكون | أقسام المطعم المختلفة (الصالة، المطبخ، المشتريات) | كل قسم مسؤول عن شؤونه | مكون الزر مسؤول عن النقر، مكون النموذج مسؤول عن الإدخال |
| Props (الخصائص) | قائمة الطلبات التي يعطيها الزبون للنادل | المكون الأب يمرر البيانات للمكون الابن | المكون الأب يمرر "اسم المستخدم" لمكون الصورة الرمزية |
| Events (الأحداث) | النادل يخطر المطبخ "يوجد طلب جديد" | المكون الابن يخطر المكون الأب بما حدث | مكون الزر يخبر المكون الأب "تم النقر عليّ" |
| State (الحالة) | "قائمة الطلبات الحالية" في المطبخ | البيانات المخزنة داخل المكون | مكون السلة يتذكر المنتجات الموجودة بداخله |
📊 ماذا يمكنك أن ترى من الجدول؟
لنفسر هذا الجدول سطرًا بسطر:
المكون: مثلما يوجد في المطعم أقسام مختلفة، تتكون صفحة الواجهة الأمامية من مكونات مختلفة. كل مكون هو جزء مستقل له مسؤوليته الخاصة.
Props: هذه هي طريقة المكون الأب في "تمرير البيانات" إلى المكون الابن. كما يخبر الزبون النادل بما يريد أن يأكل عندما يطلب، يمكن للمكون الأب أيضًا تمرير البيانات (مثل اسم المستخدم، معلومات المنتج) إلى المكون الابن عبر props. ملاحظة: props هي "أحادية الاتجاه"، يمكن فقط التمرير من الأب إلى الابن، وليس العكس.
Events: عندما يحتاج المكون الابن إلى إخطار المكون الأب (مثل النقر على زر، إرسال نموذج)، فإنه يُطلق حدثًا. كما يخطر النادل المطبخ "ابدأ الطهي" بعد استلام الطلب. هذا يحافظ على أحادية اتجاه تدفق البيانات — لا يمكن للمكون الابن تعديل بيانات المكون الأب مباشرة، بل يمكنه فقط "إرسال رسالة".
State: هذه هي "ذاكرة" المكون الداخلية. كما يحتاج المطبخ إلى تذكر الطلبات الحالية، يحتاج المكون أيضًا إلى تذكر حالته (مثل المنتجات في السلة، ما إذا كان الزر معطلًا). عندما تتغير الحالة، يُحدّث المكون الواجهة تلقائيًا.
2.2 Props و Events: "القناة الرسمية" للتواصل بين المكونات الأب والابن
في أطر الواجهات الأمامية (Vue, React)، Props و Events هما الطريقة القياسية للتواصل بين المكونات الأب والابن.
مثال Vue:
<!-- Parent.vue - المكون الأب -->
<template>
<div>
<!-- مثل إعطاء النادل قائمة الطلبات، مرر البيانات عبر props -->
<Child
:user-name="currentUser.name"
:is-admin="currentUser.isAdmin"
@delete-user="handleDelete"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const currentUser = ref({
name: '张三',
isAdmin: true
})
const handleDelete = (userId) => {
console.log('حذف المستخدم:', userId)
// معالجة منطق الحذف
}
</script><!-- Child.vue - المكون الابن -->
<template>
<div class="user-card">
<h3>{{ userName }}</h3>
<span v-if="isAdmin" class="badge">مدير</span>
<button @click="requestDelete">حذف المستخدم</button>
</div>
</template>
<script setup>
// استقبال البيانات الممررة من المكون الأب
const props = defineProps({
userName: { type: String, required: true },
isAdmin: { type: Boolean, default: false }
})
// تعريف الأحداث التي يمكن إطلاقها
const emit = defineEmits(['delete-user'])
const requestDelete = () => {
// إخطار المكون الأب عبر الحدث
emit('delete-user', props.userName)
}
</script>💡 المبدأ الأساسي
Props للأسفل، Events للأعلى — هذا هو القانون الذهبي للتواصل بين المكونات.
- المكون الأب يمرر البيانات إلى المكون الابن عبر props (مثل توزيع المهام على المرؤوسين)
- المكون الابن يخطر المكون الأب بما حدث عبر events (مثل تقرير المرؤوس عن العمل)
هذا يحافظ على وضوح تدفق البيانات وأحاديته، ويتجنب فوضى "يمكن لأي أحد تعديل البيانات".
2.3 تدفق البيانات أحادي الاتجاه: لماذا لا يمكن تعديل props مباشرة؟
يقع العديد من المبتدئين في خطأ شائع: تعديل قيمة props مباشرة داخل المكون الابن.
<!-- ❌ الطريقة الخاطئة -->
<script setup>
const props = defineProps({
count: { type: Number, default: 0 }
})
// تعديل props مباشرة - هذا ممنوع!
props.count = 10 // سيؤدي إلى خطأ
</script>لماذا لا يمكن تعديل props مباشرة؟
تخيل أنك استعرت كتابًا من المكتبة (props)، ثم كتبت عليه وخربشته (تعديل props). الأشخاص الآخرون الذين يستعيرون هذا الكتاب (المكونات الأخرى) سيرون خربشاتك أيضًا، مما يسبب الفوضى. الطريقة الصحيحة هي: إذا احتجت إلى تعديل البيانات، دع المكون الأب هو من يفعل ذلك، والمكون الابن فقط "يطلب التعديل".
<!-- ✅ الطريقة الصحيحة -->
<script setup>
const props = defineProps({
count: { type: Number, default: 0 }
})
const emit = defineEmits(['update-count'])
// طلب التعديل من المكون الأب عبر الحدث
const increment = () => {
emit('update-count', props.count + 1)
}
</script>3. من "الفوضى" إلى "النظام": رحلة تطور تواصل المكونات
🤔 لماذا نحتاج إلى التطور؟
مع كبر المشروع، يصبح التواصل بين المكونات أكثر تعقيدًا. لنرى كيف تطور فريق حقيقي خطوة بخطوة نحو حل واضح لإدارة الحالة.
هذا ليس مجرد "ترقية للأدوات"، بل تغير كامل في طريقة التفكير — من "تمرير البيانات بشكل عشوائي" إلى "تصميم تدفق بيانات واضح".
3.1 الصورة الشاملة للتطور
الجدول التالي يعرض المراحل الأربع لتطور طرق تواصل المكونات، ويمكنك رؤية كيف تم حل المشاكل خطوة بخطوة:
| المرحلة | طريقة التواصل | المشاكل النموذجية | التغير الأساسي |
|---|---|---|---|
| المرحلة الأولى: التمرير الحر | التعديل المباشر، المتغيرات العامة | عدم تزامن البيانات، صعوبة التصحيح | لا توجد قواعد، أي طريقة تمرير مسموحة |
| المرحلة الثانية: Props/Events | التواصل القياسي بين الأب والابن | Props Drilling (التمرير الطبقي) | توجد قواعد، لكن التداخل العميق مزعج |
| المرحلة الثالثة: مكتبات إدارة الحالة | Vuex/Redux/Pinia | تكلفة التعلم، الكود النمطي | إدارة مركزية للبيانات، تصحيح سهل |
| المرحلة الرابعة: الحلول الحديثة | الدوال التركيبية/الذرية | تحتاج فهم مفاهيم جديدة | أكثر مرونة، أكثر إيجازًا |
📊 ماذا يمكنك أن ترى من الجدول؟
لنفسر هذا الجدول سطرًا بسطر:
المرحلة الأولى ← المرحلة الثانية: من "لا قواعد" إلى "توجد قواعد". هذه نقلة نوعية — تبدأ في استخدام تواصل props/events القياسي، ويصبح تدفق البيانات واضحًا. لكن الثمن هو أنه عندما يكون تسلسل المكونات عميقًا، يجب تمرير البيانات طبقة بطبقة، وهذا مزعج جدًا (وهو ما يسمى Props Drilling).
المرحلة الثانية ← المرحلة الثالثة: من "الإدارة الموزعة" إلى "الإدارة المركزية". تبدأ في استخدام مكتبات إدارة الحالة مثل Vuex/Redux، وتضع البيانات المشتركة في "مخزن" عام، بحيث تقرأ وتكتب جميع المكونات البيانات من هنا. هذا يحل مشكلة Props Drilling، لكن تكلفة التعلم تصبح أعلى.
المرحلة الثالثة ← المرحلة الرابعة: من "الثقيل" إلى "الخفيف". الحلول الجديدة (مثل Composition API في Vue 3، و Hooks في React) تجعل إدارة الحالة أكثر مرونة وإيجازًا. لم تعد مضطرًا لاستخدام مخزن عام، بل يمكنك تجميع وحدات حالة صغيرة حسب الحاجة.
الخلاصة: التطور ليس مجرد "استبدال أدوات أفضل"، بل ترقية كاملة لطريقة التفكير — من تمرير البيانات بشكل عشوائي، إلى تصميم تدفق بيانات واضح.
3.2 المرحلة الأولى: التمرير الحر — بداية الفوضى
لماذا نسميها "التمرير الحر"؟ لأنه في هذه المرحلة لا توجد أي قواعد، البيانات تُمرر بأي طريقة — متغيرات عامة، تعديل مباشر، نواقل أحداث في كل مكان.
سيناريو نموذجي: بيانات سلة التسوق موزعة في كل مكان
// مكون صفحة تفاصيل المنتج
export default {
data() {
return {
localCart: [] // يحتفظ بنسخته الخاصة من بيانات السلة
}
},
methods: {
addToCart(product) {
this.localCart.push(product)
// محاولة المزامنة مع المكونات الأخرى
window.cart = this.localCart // ❌ متغير عام!
}
}
}
// مكون صفحة سلة التسوق
export default {
data() {
return {
cartItems: [] // نسخة أخرى من بيانات السلة
}
},
mounted() {
// محاولة القراءة من المتغير العام
this.cartItems = window.cart || [] // ❌ غير موثوق!
}
}
// مكون شريط التنقل العلوي
export default {
data() {
return {
cartCount: 0 // نسخة ثالثة من البيانات!
}
},
mounted() {
// استطلاع دوري للتغييرات (كم هذا سخيف)
setInterval(() => {
this.cartCount = window.cart?.length || 0
}, 1000) // ❌ أداء سيء!
}
}خصائص هذه المرحلة:
- ✅ الإيجابيات: بسيطة ومباشرة، لا توجد تكلفة تعلم
- ❌ السلبيات: بيانات موزعة، صعوبة في المزامنة، صعوبة في التصحيح، فوضى عارمة
3.3 المرحلة الثانية: Props/Events — بناء القواعد
فوضى التمرير الحر جعلت الفريق يدرك: نحن بحاجة إلى قواعد. فبدأوا في استخدام طرق التواصل القياسية التي توفرها الأطر: props و events.
سيناريو نموذجي: Props Drilling (حفر الخصائص)
<!-- المكون الجد: App.vue -->
<template>
<div class="app">
<!-- تمرير معلومات المستخدم طبقة بطبقة -->
<Layout :user-name="userName" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Layout from './Layout.vue'
const userName = ref('张三')
</script><!-- الطبقة الوسطى: Layout.vue -->
<template>
<div class="layout">
<Header :user-name="userName" /> <!-- مجرد تمرير، لا يستخدم -->
<Main>
<Page :user-name="userName" /> <!-- مجرد تمرير، لا يستخدم -->
</Main>
</div>
</template>
<script setup>
const props = defineProps({
userName: String
})
</script><!-- المكان الذي يحتاجه فعليًا: Header.vue -->
<template>
<header>
<span>{{ userName }}</span> <!-- أخيرًا تم استخدامه -->
</header>
</template>
<script setup>
const props = defineProps({
userName: String
})
</script>خصائص هذه المرحلة:
- ✅ الإيجابيات: تدفق بيانات واضح، أحادي الاتجاه، سهل الفهم
- ❌ السلبيات: Props Drilling (التمرير الطبقي مزعج جدًا)، صعوبة في التواصل بين المكونات غير المرتبطة
🤔 ما هو Props Drilling؟
Props Drilling يعني: البيانات يجب أن تمر عبر العديد من المكونات الوسيطة، طبقة بطبقة، لكن هذه المكونات الوسيطة لا تستخدم هذه البيانات فعليًا.
كما لو كنت تريد توصيل طرد إلى شخص في الطابق الخامس، لكن القواعد تنص على أن كل طابق يجب أن يوقع على الاستلام. سكان الطوابق الأول والثاني والثالث والرابع فقط "يمررون الطرد"، هم لا يحتاجونه، لكن يجب أن يشاركوا. من الواضح أن هذا مزعج جدًا.
3.4 المرحلة الثالثة: مكتبات إدارة الحالة — الإدارة المركزية
أدت معاناة Props Drilling إلى ظهور مكتبات إدارة الحالة (Vuex، Redux، Pinia). فكرتها الأساسية: ضع البيانات المشتركة في "مستودع" عام، بحيث تقرأ وتكتب جميع المكونات البيانات من هنا.
سيناريو نموذجي: إدارة سلة التسوق باستخدام Pinia
// stores/cart.js - حالة سلة التسوق العامة
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// جميع بيانات سلة التسوق مركزة هنا
const items = ref([])
// خاصية محسوبة: عدد المنتجات
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
// دالة: إضافة منتج
const addItem = (product) => {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
return {
items,
itemCount,
addItem
}
})<!-- مكون صفحة تفاصيل المنتج -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const addToCart = (product) => {
cart.addItem(product) // استدعاء مباشر، لا حاجة للتمرير الطبقي
}
</script><!-- مكون شريط التنقل العلوي -->
<template>
<header>
<span>سلة التسوق ({{ cart.itemCount }})</span>
</header>
</template>
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore() // قراءة مباشرة، مزامنة تلقائية
</script>خصائص هذه المرحلة:
- ✅ الإيجابيات: إدارة مركزية للبيانات، حل Props Drilling، أدوات تصحيح قوية
- ❌ السلبيات: تكلفة التعلم، الحاجة لكتابة كود إضافي (كود نمطي)، قد تكون مبالغة في التصميم للمشاريع البسيطة
3.5 المرحلة الرابعة: الحلول الحديثة — المرونة والإيجاز
مكتبات إدارة الحالة قوية، لكن لديها مشكلة "استخدام المدفع لقتل بعوضة". بالنسبة للمشاريع الصغيرة والمتوسطة، ظهرت حلول أكثر مرونة وخفة.
سيناريو نموذجي: إعادة استخدام منطق الحالة باستخدام Composable/Hooks
// composables/useCart.js - منطق سلة تسوق قابل لإعادة الاستخدام
import { ref, computed } from 'vue'
export function useCart() {
const items = ref([])
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const addItem = (product) => {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
return {
items,
itemCount,
addItem
}
}<!-- الاستخدام في أي مكون -->
<script setup>
import { useCart } from '@/composables/useCart'
// كل استدعاء ينشئ نسخة حالة جديدة
// مناسب للحالة المحلية داخل المكون
const { items, itemCount, addItem } = useCart()
</script>خصائص هذه المرحلة:
- ✅ الإيجابيات: مرنة، خفيفة، قابلة للتجميع، تستخدم حسب الحاجة
- ❌ السلبيات: تحتاج فهم التفكير التركيبي، المشاركة بين المكونات تحتاج معالجة إضافية
4. تفصيل مكتبات إدارة الحالة: Vuex مقابل Pinia مقابل Redux
🤔 كيف تختار مكتبة إدارة الحالة؟
عند مواجهة مكتبات إدارة الحالة المختلفة، قد تحتار: أي واحدة تختار؟
في الواقع، لا توجد مكتبة "أفضل"، بل هناك "الأنسب". عند الاختيار، ضع في اعتبارك هذه العوامل:
- أي إطار تستخدم؟ Vue استخدم Pinia، React استخدم Redux/Zustand
- ما حجم المشروع؟ المشاريع الصغيرة استخدم Composable، المشاريع الكبيرة استخدم مكتبة إدارة الحالة
- خبرة الفريق؟ اختر ما يعرفه الفريق، أو ما تكلفة تعلمه منخفضة
المحتوى التالي سيشرح بالتفصيل خصائص وسيناريوهات استخدام مكتبات إدارة الحالة الرئيسية.
4.1 مقارنة مكتبات إدارة الحالة الرئيسية
| الخاصية | Redux | Vuex | Pinia | Zustand |
|---|---|---|---|---|
| الإطار المناسب | React | Vue | Vue | React |
| منحنى التعلم | حاد | متوسط | لطيف | لطيف |
| الكود النمطي | كثير | متوسط | قليل | قليل جدًا |
| TypeScript | جيد | جيد | ممتاز | ممتاز |
| أدوات التصحيح | قوية | جيدة | ممتازة | جيدة |
| سيناريو الاستخدام | مشاريع كبيرة | مشاريع Vue 2/3 متوسطة وكبيرة | مشاريع Vue 3 جديدة | مشاريع React صغيرة ومتوسطة |
📊 ماذا يمكنك أن ترى من الجدول؟
لنفسر هذا الجدول سطرًا بسطر:
Redux: مكتبة إدارة الحالة الكلاسيكية في بيئة React. إيجابياتها: قواعد صارمة، أدوات تصحيح قوية. سلبياتها: كود نمطي كثير، منحنى تعلم حاد. مناسبة للمشاريع الكبيرة والفرق التي تحتاج قواعد صارمة.
Vuex: مكتبة إدارة الحالة الرسمية في حقبة Vue 2. فلسفة التصميم مشابهة لـ Redux، لكنها أكثر تناغمًا مع نظام التفاعلية في Vue. لا يزال يمكن استخدامها الآن، لكن للمشاريع الجديدة يُنصح بـ Pinia.
Pinia: مكتبة إدارة الحالة من الجيل الجديد الموصى بها رسميًا لـ Vue 3. قواعد بسيطة، دعم ممتاز لـ TypeScript، تكلفة تعلم منخفضة. هذا هو الخيار الأول لمشاريع Vue 3.
Zustand: مكتبة إدارة حالة خفيفة في بيئة React. API بسيطة للغاية، تقريبًا لا يوجد كود نمطي. مناسبة لمشاريع React الصغيرة والمتوسطة.
Pinia
Intuitive, type-safe, flexible Vue Store
4.2 Pinia عمليًا: الخيار الموصى به لـ Vue 3
Pinia هي مكتبة إدارة الحالة الموصى بها رسميًا من فريق Vue، مصممة خصيصًا لـ Vue 3. إنها أبسط وأسهل استخدامًا من Vuex.
لماذا سميت Pinia؟
Pinia هي كلمة إسبانية تعني "الأناناس". الأناناس فاكهة مكونة من العديد من الزهيرات الصغيرة، كل زهيرة مستقلة، لكنها ككل تشكل وحدة موحدة. هذا يشبه تمامًا فلسفة تصميم Pinia — كل store مستقل، لكن يمكن استخدامها معًا بشكل تركيبي.
المفاهيم الأساسية:
عرض مثال الكود الكامل
// stores/user.js - إدارة حالة المستخدم
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 1. State: تخزين البيانات
const userInfo = ref(null)
const isLoggedIn = computed(() => !!userInfo.value)
// 2. Actions: دوال تعديل البيانات
const login = async (username, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
const user = await response.json()
userInfo.value = user // تعديل مباشر، Pinia يتولى التفاعلية
}
const logout = () => {
userInfo.value = null
}
// 3. Getters: خصائص محسوبة
const displayName = computed(() => {
return userInfo.value?.name || 'زائر'
})
return {
userInfo,
isLoggedIn,
login,
logout,
displayName
}
})الاستخدام في المكونات:
<template>
<div class="user-panel">
<span v-if="user.isLoggedIn">مرحبًا، {{ user.displayName }}</span>
<button v-if="user.isLoggedIn" @click="user.logout">تسجيل الخروج</button>
<button v-else @click="showLoginDialog">تسجيل الدخول</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
// الحصول على store مباشرة، كل المحتوى تفاعلي
const user = useUserStore()
const showLoginDialog = () => {
// إظهار حوار تسجيل الدخول...
}
</script>مزايا Pinia:
| الميزة | الشرح | مقارنة مع Vuex |
|---|---|---|
| API بسيطة | لا حاجة لـ mutations، تعديل مباشر لـ state | Vuex يحتاج فصل mutations و actions |
| صديقة لـ TypeScript | استدلال نوعي أصلي، لا حاجة لإعدادات إضافية | Vuex يحتاج تعريفات أنواع معقدة |
| تقسيم تلقائي للوحدات | كل ملف store يصبح وحدة تلقائيًا | Vuex يحتاج إعداد namespaced يدويًا |
| حجم أصغر | حوالي 1KB بعد الحزم | Vuex حوالي 3KB |
// stores/counter.js
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})4.3 Redux عمليًا: الخيار الكلاسيكي لـ React
Redux هي مكتبة إدارة الحالة الأكثر كلاسيكية في بيئة React، وتتميز بتدفق البيانات الأحادي الصارم.
لماذا سمي Redux؟
Redux هو اختصار لـ "Reduced Flux". Flux هو نمط معمارية تطبيق اقترحته Facebook مبكرًا، وRedux بسط مفاهيم Flux، لذا سمي "Reduced Flux".
المبادئ الأساسية:
- مصدر بيانات واحد: حالة التطبيق بالكامل مخزنة في شجرة كائن واحدة
- الحالة للقراءة فقط: الطريقة الوحيدة لتغيير الحالة هي إطلاق action
- استخدام دوال نقية للتعديل: Reducer يجب أن يكون دالة نقية
عرض مثال الكود الكامل
// 1. تعريف Action Types
const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'
// 2. تعريف Action Creators
const addTodo = (text) => ({
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
})
const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: { id }
})
// 3. تعريف Reducer (دالة نقية)
const initialState = {
todos: []
}
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
}
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
}
default:
return state
}
}
// 4. إنشاء Store
import { createStore } from 'redux'
const store = createStore(todoReducer)الاستخدام في React:
import { useSelector, useDispatch } from 'react-redux'
function TodoList() {
// قراءة الحالة
const todos = useSelector(state => state.todos)
// الحصول على دالة dispatch
const dispatch = useDispatch()
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
)
}إيجابيات وسلبيات Redux:
| الإيجابيات | السلبيات |
|---|---|
| تدفق بيانات صارم، سهل التصحيح | كود نمطي كثير، منحنى تعلم حاد |
| تصحيح السفر عبر الزمن (Time Travel) | الحالات البسيطة تحتاج أيضًا كتابة كود كثير |
| بيئة غنية من البرمجيات الوسيطة | غير مناسب للمشاريع الصغيرة |
| تحديثات حالة قابلة للتنبؤ | يحتاج فهم مفاهيم البرمجة الوظيفية |
// Zustand Store
import { create } from 'zustand'
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({
bears: state.bears + 1
}))
}))
// Use inside a component
function BearCounter() {
const bears = useStore((state) => state.bears)
return <div>{bears} bears around here</div>
}5. دليل عملي: كيف تصمم إدارة الحالة؟
🤔 متى تحتاج مكتبة إدارة الحالة؟
ليس كل مشروع يحتاج مكتبة إدارة حالة. قبل إدخالها، اسأل نفسك بعض الأسئلة:
كم عدد المكونات التي تحتاج مشاركة هذه البيانات؟
- إذا كان فقط 2-3 مكونات، استخدم props/events يكفي
- إذا كان 5+ مكونات، فكر في مكتبة إدارة الحالة
هل هذه البيانات تتغير كثيرًا؟
- إذا كانت شبه ثابتة (مثل معلومات المستخدم)، استخدم Provide/Inject
- إذا كانت تتغير كثيرًا (مثل سلة التسوق)، استخدم مكتبة إدارة الحالة
ما حجم الفريق؟
- فردي أو فريق صغير: حل بسيط يكفي
- فريق كبير: تحتاج قواعد صارمة وأدوات تصحيح قوية
تذكر: ابدأ بسيطًا، ورقِّ حسب الحاجة.
5.1 مبادئ تصميم الحالة
بغض النظر عن حل إدارة الحالة الذي تختاره، يجب أن تتبع هذه المبادئ:
المبدأ الأول: مصدر بيانات واحد
نفس البيانات يجب أن تخزن في مكان واحد فقط. لا تعرّف نفس البيانات بشكل مكرر في مكونات متعددة.
// ❌ خطأ: البيانات موزعة في كل مكان
const ProductDetail = { cart: [] }
const CartPage = { items: [] }
const Header = { count: 0 }
// ✅ صحيح: البيانات مدارة مركزيًا
const cartStore = { items: [] } // مصدر البيانات الوحيدالمبدأ الثاني: عدم القابلية للتغيير
عند تعديل الحالة، يجب إنشاء كائن جديد، بدلًا من تعديل الكائن الأصلي مباشرة.
// ❌ خطأ: تعديل مباشر
state.items.push(newItem)
// ✅ صحيح: إنشاء كائن جديد
state.items = [...state.items, newItem]المبدأ الثالث: الحالة للأعلى، الأحداث للأسفل
الحالة المشتركة يجب أن توضع في أقرب مكون سلف مشترك أو في store عام، وليس موزعة في المكونات الأبناء.
<!-- ❌ خطأ: الحالة في المكون الابن -->
<Parent>
<Child :data="childData" @update="childData = $event" />
</Parent>
<!-- ✅ صحيح: الحالة في المكون الأب -->
<Parent>
<Child :data="parentData" @update="parentData = $event" />
</Parent>5.2 حالة عملية: تصميم حالة سلة تسوق لمتجر إلكتروني
لنطبق المعرفة السابقة بشكل شامل، ونصمم حل إدارة حالة لسلة تسوق متجر إلكتروني.
تحليل المتطلبات:
- صفحة قائمة المنتجات يمكنها إضافة منتجات إلى السلة
- صفحة سلة التسوق يمكنها عرض وتعديل الكمية وحذف المنتجات
- شريط التنقل العلوي يعرض عدد منتجات السلة
- دعم تحديد/إلغاء تحديد المنتجات، وحساب السعر الإجمالي للمنتجات المحددة
- استمرار البيانات في localStorage
تصميم الحالة (Pinia):
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// ============ State (الحالة) ============
const items = ref([]) // قائمة منتجات السلة
const selectedIds = ref([]) // معرفات المنتجات المحددة
// استعادة البيانات من localStorage
const initFromStorage = () => {
const stored = localStorage.getItem('cart')
if (stored) {
try {
const data = JSON.parse(stored)
items.value = data.items || []
selectedIds.value = data.selectedIds || []
} catch (e) {
console.error('فشل قراءة بيانات السلة:', e)
}
}
}
// الاستمرار في localStorage
const persist = () => {
localStorage.setItem('cart', JSON.stringify({
items: items.value,
selectedIds: selectedIds.value
}))
}
// ============ Getters (خصائص محسوبة) ============
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const selectedItems = computed(() =>
items.value.filter(item => selectedIds.value.includes(item.id))
)
const selectedTotalPrice = computed(() =>
selectedItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// ============ Actions (الدوال) ============
const addItem = (product) => {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity += product.quantity || 1
} else {
items.value.push({
...product,
quantity: product.quantity || 1
})
}
persist()
}
const updateQuantity = (productId, quantity) => {
const item = items.value.find(item => item.id === productId)
if (item) {
if (quantity <= 0) {
removeItem(productId)
} else {
item.quantity = quantity
persist()
}
}
}
const removeItem = (productId) => {
items.value = items.value.filter(item => item.id !== productId)
selectedIds.value = selectedIds.value.filter(id => id !== productId)
persist()
}
const toggleSelection = (productId) => {
const index = selectedIds.value.indexOf(productId)
if (index > -1) {
selectedIds.value.splice(index, 1)
} else {
selectedIds.value.push(productId)
}
persist()
}
// التهيئة
initFromStorage()
return {
// State
items,
selectedIds,
// Getters
itemCount,
totalPrice,
selectedItems,
selectedTotalPrice,
// Actions
addItem,
updateQuantity,
removeItem,
toggleSelection
}
})الاستخدام في المكونات:
<!-- صفحة تفاصيل المنتج: ProductDetail.vue -->
<template>
<div class="product-detail">
<h2>{{ product.name }}</h2>
<p class="price">¥{{ product.price }}</p>
<button @click="addToCart">أضف إلى السلة</button>
</div>
</template>
<script setup>
import { useCartStore } from '@/stores/cart'
const props = defineProps({
product: Object
})
const cart = useCartStore()
const addToCart = () => {
cart.addItem({
id: props.product.id,
name: props.product.name,
price: props.product.price
})
}
</script><!-- شريط التنقل العلوي: Header.vue -->
<template>
<header class="header">
<div class="logo">متجري</div>
<nav>
<RouterLink to="/">الرئيسية</RouterLink>
<RouterLink to="/cart">
سلة التسوق ({{ cart.itemCount }})
</RouterLink>
</nav>
</header>
</template>
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore() // استخدام مباشر، استجابة تلقائية للتغييرات
</script>6. الأخطاء الشائعة ودليل تجنبها
⚠️ هذه الأخطاء، 90% من المبتدئين يقعون فيها
في ممارسة إدارة الحالة، بعض الأخطاء شائعة جدًا. دعني ألخص أكثر الأخطاء شيوعًا، وكيفية تجنبها.
6.1 الخطأ الأول: تعديل Props أو State مباشرة
الكود الخاطئ:
// ❌ تعديل props مباشرة
props.user.name = 'لي سي'
// ❌ تعديل state في Vuex مباشرة
store.state.user.name = 'لي سي'
// ❌ تعديل عنصر مصفوفة مباشرة
state.items[0].name = 'اسم جديد'لماذا هذا غير صحيح؟
أطر الواجهات الأمامية (Vue/React) تحتاج إلى "تتبع" تغييرات البيانات لتتمكن من تحديث الواجهة تلقائيًا. إذا عدلت الكائن أو المصفوفة مباشرة، قد لا يتمكن الإطار من اكتشاف التغيير، مما يؤدي إلى عدم تحديث الواجهة.
الطريقة الصحيحة:
// ✅ Vue 3 / Pinia: تعديل مباشر للخصائص العلوية
store.user.name = 'لي سي' // Pinia يتولى التفاعلية تلقائيًا
// ✅ Vue 2 / Vuex: عبر mutation
mutations: {
UPDATE_USER_NAME(state, newName) {
state.user.name = newName
}
}
// ✅ تعديل المصفوفة: إنشاء مصفوفة جديدة
state.items = state.items.map((item, index) =>
index === 0 ? { ...item, name: 'اسم جديد' } : item
)6.2 الخطأ الثاني: تعديل الحالة داخل Getter
الكود الخاطئ:
// ❌ تعديل الحالة داخل getter
getters: {
doubleCount(state) {
state.count *= 2 // تأثير جانبي!
return state.count
}
}لماذا هذا غير صحيح؟
Getter يجب أن يكون "دالة نقية"، مسؤولة فقط عن الحساب وإرجاع القيمة، ولا يجب أن يكون له أي آثار جانبية (تعديل الحالة). إذا عدلت الحالة داخل getter، فقد يؤدي ذلك إلى حلقات لا نهائية ومشاكل يصعب تصحيحها.
الطريقة الصحيحة:
// ✅ Getter يحسب فقط، لا يعدل
getters: {
doubleCount(state) {
return state.count * 2
}
}
// ✅ إذا احتجت التعديل، استخدم action
actions: {
doubleCountAndSave({ commit }) {
commit('SET_DOUBLE_COUNT')
}
}6.3 الخطأ الثالث: نسيان تنظيف مستمعي الأحداث
الكود الخاطئ:
// ❌ نسيان إلغاء الاشتراك
export default {
created() {
EventBus.$on('cart-updated', this.handleCartUpdate)
}
// المكون تدمر، لكن المستمع ما زال موجودًا!
}لماذا هذا غير صحيح؟
إذا تدمر المكون لكن مستمع الحدث ما زال موجودًا، فسيؤدي ذلك إلى تسرب في الذاكرة (الذاكرة المحجوزة لا يمكن تحريرها). في تطبيقات الصفحة الواحدة، يتنقل المستخدم باستمرار بين الصفحات، وهذه المستمعات غير النظيفة تتراكم أكثر فأكثر، مما يؤدي في النهاية إلى بطء الصفحة.
الطريقة الصحيحة:
// ✅ إلغاء الاشتراك في الوقت المناسب
export default {
created() {
EventBus.$on('cart-updated', this.handleCartUpdate)
},
beforeUnmount() { // Vue 3 يستخدم beforeUnmount، Vue 2 يستخدم beforeDestroy
EventBus.$off('cart-updated', this.handleCartUpdate)
}
}6.4 الخطأ الرابع: الإفراط في استخدام إدارة الحالة
الكود الخاطئ:
// ❌ وضع كل الحالات في store
const store = useStore()
store.inputValue = 'إدخال المستخدم'
store.isModalOpen = true
store.currentTab = 'profile'لماذا هذا غير صحيح؟
ليس كل الحالات تحتاج أن توضع في store العام. إذا كانت حالة تستخدم فقط في مكون واحد (مثل قيمة حقل الإدخال، حالة فتح/إغلاق النافذة المنبثقة)، فضعها داخل المكون. الإفراط في استخدام إدارة الحالة يجعل الكود معقدًا.
الطريقة الصحيحة:
// ✅ الحالة المحلية تدار داخل المكون
const inputValue = ref('')
// ✅ فقط الحالات التي تحتاج مشاركة توضع في store
const userInfo = useUserStore() // مكونات متعددة تحتاج معلومات المستخدم
const cart = useCartStore() // مكونات متعددة تحتاج بيانات السلة7. الخلاصة والتوصيات
7.1 مراجعة النقاط الأساسية
لنستخدم جدولًا لمراجعة المفاهيم الأساسية للمكونات وإدارة الحالة:
| المفهوم | شرح في جملة واحدة | المشكلة التي يحلها | الأدوات النموذجية |
|---|---|---|---|
| المكونات | تقسيم الواجهة إلى أجزاء مستقلة قابلة لإعادة الاستخدام | إعادة استخدام الكود، فصل المسؤوليات | مكونات Vue/React |
| Props | المكون الأب يمرر البيانات للمكون الابن | التواصل بين الأب والابن | مدمج في Vue/React |
| Events | المكون الابن يخطر المكون الأب بما حدث | التواصل بين الابن والأب | مدمج في Vue/React |
| State | البيانات المخزنة داخل المكون | تذكر حالة المكون | مدمج في Vue/React |
| مكتبة إدارة الحالة | إدارة مركزية للحالة العامة المشتركة | التواصل بين المكونات، Props Drilling | Pinia، Redux، Zustand |
| مصدر البيانات الوحيد | نفس البيانات تخزن في مكان واحد فقط | عدم اتساق البيانات، صعوبة المزامنة | المبدأ الأساسي لمكتبات إدارة الحالة |
7.2 توصيات الاختيار حسب السيناريو
| السيناريو | الحل الموصى به | السبب |
|---|---|---|
| تواصل المكونات الأب والابن | Props + Events | مدمج في الإطار، بسيط ومباشر |
| تمرير القيم عبر المستويات | Provide / Inject | تجنب التمرير الطبقي |
| الحالة المحلية داخل المكون | ref / useState | بسيط، لا حاجة لأدوات إضافية |
| مشروع Vue متوسط | Pinia | موصى به رسميًا، تكلفة تعلم منخفضة |
| مشروع React متوسط | Zustand | بسيط جدًا، لا كود نمطي |
| مشروع Vue كبير | Pinia + قواعد | مرن وقابل للتوسع |
| مشروع React كبير | Redux Toolkit | قواعد صارمة، بيئة غنية |
| إعادة استخدام المنطق بين المكونات | Composable / Hooks | مرن، قابل للتجميع |
7.3 توصيات التعلم
للمبتدئين:
- أتقن الأساسيات أولًا: افهم المفاهيم الأساسية مثل props و events و state
- ابدأ من مشاريع صغيرة: لا تبدأ مباشرة بمكتبات إدارة الحالة
- اكتب كودًا أكثر: مهما تعلمت من النظرية، لا شيء يضاهي الممارسة العملية
للمتقدمين:
- اقرأ الكود المصدري: افهم كيفية عمل Pinia/Redux
- تعلم الأنماط: افهم أنماط التصميم الشائعة (مثل نمط المراقب، نمط النشر والاشتراك)
- تابع البيئة: تعلم الأدوات ذات الصلة (مثل DevTools، البرمجيات الوسيطة)
تذكر هذه المبادئ الأساسية:
- ابدأ بسيطًا: لا تدخل مكتبات إدارة حالة معقدة قبل الأوان
- مصدر بيانات واحد: تجنب تخزين نفس البيانات في أماكن متعددة
- عدم القابلية للتغيير: أنشئ كائنات جديدة عند تعديل الحالة، بدلًا من التعديل المباشر
- اختر حسب الحاجة: اختر الحل المناسب بناءً على حجم المشروع وظروف الفريق
آمل أن يساعدك هذا المقال في بناء فهم شامل للمكونات وإدارة الحالة. عندما تواجه مشاكل تدفق بيانات معقدة في مشاريعك الفعلية، ستعرف من أين تبدأ، وكيف تصمم، وكيف تنفذ.