تعلم Vuex في 5 دقائق: دليل عملي لإدارة الحالة في تطبيقات Vue.js
في عالم تطوير الواجهات الأمامية الحديث، تُعد إدارة الحالة (State Management) تحديًا رئيسيًا، خاصةً في التطبيقات كبيرة الحجم. هنا يأتي دور Vuex، وهو نمط ومكتبة لإدارة الحالة مصممة خصيصًا لتطبيقات Vue.js. يهدف هذا الدليل إلى تزويدك بفهم أساسي لـ Vuex من خلال بناء تطبيق عملي يتيح للمستخدمين إضافة أنشطة والتصويت عليها.
ما هو Vuex؟
وفقًا للوثائق الرسمية لـ Vue.js، يُعرف Vuex بأنه:
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
بمعنى آخر، يعمل Vuex كمتجر مركزي (Centralized Store) لجميع المكونات في تطبيقك، مع قواعد صارمة تضمن أن الحالة (State) لا يمكن تغييرها إلا بطريقة يمكن التنبؤ بها. يفترض هذا الدليل أن لديك بعض الإلمام بـ Vue.js، وسنتطرق باختصار إلى ميزات مثل props والمكونات (components) والربط (bindings) دون الخوض في تفاصيلها.
إعداد بيئة العمل
لتبسيط عملية الإعداد، سنستخدم ملف HTML واحدًا حيث يمكن كتابة جميع الأكواد. سيتم استيراد مكتبتي Vue و Vuex عبر شبكة توصيل المحتوى (CDN) باستخدام وسوم <script>. يمكنك إضافة ملف CSS خاص بك أو نسخه من أي بيئة تطوير تفاعلية.
<!DOCTYPE html >
< html lang = "en" >
< head >
< title > Activity Voter </ title >
< script src = "https://cdn.jsdelivr.net/npm/vue/dist/vue.js" > </ script >
< script src = "https://cdn.jsdelivr.net/npm/vuex/dist/vuex.js" > </ script >
< style > /* أضف CSS هنا */ </ style >
</ head >
< body >
< div id = "app" > </ div >
</ body >
< script > /* أضف كود Vue هنا */ </ script >
</ html >
فكرة التطبيق: مصوت الأنشطة
سنقوم ببناء تطبيق تصويت بسيط وممتع، وهو مثالي عندما تكون مع مجموعة من الأصدقاء وتواجهون صعوبة في تحديد نشاط معين. ستتمحور الوظائف حول قدرة المستخدم على إدخال نشاط جديد، ثم سيكون لكل نشاط أزرار تصويت لزيادة أو تقليل تقييمه.
البدء: هيكلة HTML الأولية
لنبدأ بإنشاء هيكل HTML الأولي لتطبيقنا. سنستخدم هذا التخطيط كنقطة بداية قبل استخراجه إلى مكونات منفصلة وإضافة الوظائف التفاعلية إليه.
< div id = "app" >
< h1 > Activity voter </ h1 >
< form >
< input type = "text" placeholder = "Add Activity" />
< button id = "button" > Add Activity </ button >
</ form >
< ul >
< li >
< span > Go Snowboarding </ span >
< span > ? </ span >
< button > ? </ button > 5 < button > ? </ button >
</ span >
</ li >
</ ul >
</ div >

إضافة متجر Vuex ببيانات أساسية
يبدأ كل شيء في Vuex بالمتجر (Store). المتجر هو المكان الذي نحتفظ فيه بحالة (State) تطبيقنا. لنقم بتهيئة المتجر وإضافة بعض البيانات الأولية.
< script >
Vue.use(Vuex);
const store = new Vuex.Store({
});
new Vue({
el : "#app" ,
store
});
</ script >
الآن، لنضف بعض البيانات الثابتة إلى المتجر. ستتضمن هذه البيانات نشاطًا واحدًا ومصفوفة تحتوي على رمز تعبيري واحد (emoji) لعرض مشاعرنا تجاه النشاط.
< script >
Vue.use(Vuex);
const store = new Vuex.Store({
state : {
activities : [{
name : "go snowboarding" ,
rating : 5
}],
emojis : [
"?"
]
}
});
new Vue({
el : "#app" ,
store
});
</ script >
ربط الحالة التفاعلية باستخدام mapState
للسماح لحالتنا بالتغير بشكل تفاعلي، يمكننا استخدام دالة المساعد Vuex.mapState للتعامل مع خصائص الحالة المحسوبة (computed state properties) نيابة عنا.
new Vue({
el : "#app" ,
store,
computed : Vuex.mapState([
"activities" ,
"emojis"
])
});
إضافة مكون النشاط (Activity Component)
الآن بعد أن أصبحت لدينا الأنشطة داخل حالتنا، لنقم بعرض مكون منفصل لكل من هذه الأنشطة. سيحتاج كل مكون إلى خصائص (props) مثل activity و emojis.
Vue.component( "activity-item" , {
props : [
"activity" ,
"emojis"
],
template : `
<li>
<span>{{ activity.name }} <span>{{ emojis[0] }}</span>
<button>?</button> {{activity.rating}} <button>?</button>
</span>
</li>
`
});
داخل المكون الرئيسي app، يمكننا الآن استخدام المكون الجديد الذي أنشأناه مع جميع الروابط المناسبة لـ activity و emojis. للتذكير السريع، إذا أردنا التكرار على مصفوفة وعرض مكون لكل عنصر فيها في Vue، يمكننا استخدام ربط v-for.
< div id = "app" >
< h1 > Activity voter </ h1 >
< form >
< input type = "text" placeholder = "Add Activity" />
< button id = "button" > Add Activity </ button >
</ form >
< ul >
< activity-item v-for = "item in activities" v-bind:activity = "item" v-bind:emojis = "emojis" v-bind:key = "item.name" >
</ activity-item >
</ ul >
</ div >

إضافة الطفرات (Mutations) إلى المتجر
إذا أردنا تحديث المتجر في Vuex، يمكننا استخدام الطفرات (mutations). في الوقت الحالي، سنقوم فقط بتسجيل رسالة في console.log للإشارة إلى حدوث طفرة، وسنقوم بتنفيذها لاحقًا.
const store = new Vuex.Store({
state : {
activities : [
{
name : "go snowboarding" ,
rating : 5
},
],
emojis : [
"?"
]
},
mutations : {
increment(state, activityName) {
console
.log( 'increment' );
},
decrement(state, activityName) {
console
.log( 'decrement' );
},
}
});
كيفية تشغيل الطفرة (Triggering a Mutation)
لتشغيل طفرة، نقوم باستدعاء الدالة commit على الكائن $store مع اسم الطفرة التي نريد تنفيذها. أي وسائط (arguments) بعد اسم الطفرة تُعامل كوسائط للطفرة الملتزم بها.
new Vue({
el : "#app" ,
store,
data() {
return {
activityName : ""
};
},
computed : Vuex.mapState([
"activities" ,
"emojis"
]),
methods : {
increment(activityName) {
this
.$store.commit( "increment" , activityName);
},
decrement(activityName) {
this
.$store.commit( "decrement" , activityName);
}
}
});
إضافة الوظائف إلى المكون
يحتوي كل عنصر activity-item على أزرار تصويت تحتاج إلى استدعاء increment و decrement عند النقر عليها. يمكننا تمرير هذه الدوال كخصائص (props). لنقم الآن بربط دوالنا بالخصائص.
< activity-item v-for = "item in activities" v-bind:increment = "increment" v-bind:decrement = "decrement" v-bind:activity = "item" v-bind:emojis = "emojis" v-bind:key = "item.name" >
</ activity-item >
ولا ننسى توفير activity.name كوسيط لكلتا الدالتين.
Vue.component( "activity-item" , {
props : [
"activity" ,
"emojis" ,
"increment" ,
"decrement"
],
template : `
<li>
<span>{{ activity.name }} <span>{{ emojis[0] }}</span>
<button @click="decrement(activity.name)">?</button> {{activity.rating}} <button @click="increment(activity.name)">?</button>
</span>
</li>
`
});
وهكذا اكتمل التدفق! يمكننا الآن رؤية رسالة console.log في وحدة التحكم عند النقر على الأزرار.

تطبيق وظيفة العداد
لنقم الآن بتطبيق وظيفة العداد الفعلية. أولاً، نحتاج إلى العثور على النشاط بواسطة اسمه، ثم تحديث تقييمه (rating).
mutations: {
increment(state, activityName) {
state.activities
.filter(
activity => activity.name === ` ${activityName} `
)
.map(
activity => activity.rating++
);
},
decrement(state, activityName) {
state.activities
.filter(
activity => activity.name === ` ${activityName} `
)
.map(
activity => activity.rating--
);
}
}
ممتاز، أصبح بإمكاننا الآن التصويت على الأنشطة وتحديث تقييماتها بشكل صحيح.

استخدام حقل الإدخال لإضافة نشاط
بالطبع، نحتاج أيضًا إلى القدرة على إضافة أنشطة جديدة. لننشئ طفرة جديدة للمتجر تقوم بإضافة نشاط إلى قائمة الأنشطة الموجودة، مع اسم سنحصل عليه لاحقًا من حقل الإدخال وتقييم افتراضي يبلغ 0.
mutations: {
...
addActivity(state, name) {
state.activities.push({
name,
rating : 0
});
}
}
داخل قسم methods، يمكننا الآن الالتزام (commit) بنشاط جديد إلى المتجر.
methods: {
...
addActivity(activityName) {
this
.$store.commit( "addActivity" , activityName);
}
}
تطبيق وظيفة إرسال النموذج
لنربط دالة الإرسال (submit) بنموذج HTML الخاص بنا.
< form @ submit = "onSubmit" >
< input type = "text" placeholder = "Add Activity" v-model = "activityName" />
< button id = "button" > Add Activity </ button >
</ form >
يمكننا الآن إضافة دالة onSubmit إلى methods. داخلها، سنستخدم دالة addActivity الموجودة لدينا، وفي النهاية، سنعيد تعيين activityName في حقل الإدخال إلى سلسلة نصية فارغة.
methods: {
...
onSubmit(e) {
e.preventDefault();
this
.addActivity( this
.activityName);
this
.activityName = "" ;
}
}
نستدعي e.preventDefault() لتجنب إعادة تحميل النموذج عند كل إضافة لنشاط جديد.

تحسين الرموز التعبيرية والحالة الأولية
تعمل جميع العدادات الآن ويتم تحديث الحقل. يبدو غريبًا بعض الشيء أن لدينا رمزًا تعبيريًا واحدًا فقط لجميع الأنشطة، بغض النظر عن تقييمها. لنقم بإعادة كتابة الرموز التعبيرية إلى كائن (object) مع بعض الوصف لما تعكسه هذه الحالات المزاجية، ولننظف الحالة الحالية بحيث نبدأ بدون أنشطة.
state: {
activities : [],
emojis : {
yay : "?" ,
nice : "?" ,
meh : "?" ,
argh : "?" ,
hateIt : "?"
}
},
...
وكلمسة أخيرة، يمكننا عرض رموز تعبيرية مختلفة اعتمادًا على تقييم النشاط.
Vue.component( "activity-item" , {
props : [
"activity" ,
"emojis" ,
"increment" ,
"decrement"
],
template : `
<li>
<span>{{ activity.name }} <span v-if="activity.rating <= -5">{{ emojis.hateIt }}</span>
<span v-else-if="activity.rating <= -3">{{ emojis.argh }}</span>
<span v-else-if="activity.rating < 3">{{ emojis.meh }}</span>
<span v-else-if="activity.rating < 5">{{ emojis.nice }}</span>
<span v-else>{{ emojis.yay }}</span>
<button @click="decrement(activity.name)">?</button> {{activity.rating}} <button @click="increment(activity.name)">?</button>
</span>
</li>
`
});
الآن نبدأ بتطبيق فارغ تمامًا، وهو ما كنا نريده.

وإذا أعدنا إضافة النشاطين اللذين كانا لدينا في التطبيق، وصوتنا على تقييماتهما، فسنرى رموزًا تعبيرية تعكس مشاعرنا تجاه الأنشطة!

الخلاصة التقنية
يُعد Vuex حلًا فعالًا لإدارة الحالة المعقدة في تطبيقات Vue.js، حيث يوفر بنية منظمة ومتوقعة لتدفق البيانات. من خلال المتجر المركزي (Store) والطفرات (Mutations)، يمكن للمطورين التحكم في تحديثات الحالة بطريقة آمنة وقابلة للتتبع، مما يقلل من الأخطاء ويزيد من قابلية صيانة الكود. هذا الدليل قدم نظرة عملية على كيفية دمج Vuex في تطبيق بسيط، موضحًا قوته في بناء واجهات مستخدم تفاعلية وديناميكية مع الحفاظ على فصل الاهتمامات (Separation of Concerns). فهم هذه المفاهيم الأساسية يمهد الطريق لتطوير تطبيقات Vue.js أكثر تعقيدًا وقوة.