في عام ٢٠٢٣، هاجر ٨٧٪ من مشاريع Vue الكبيرة من Vuex إلى Pinia. لماذا؟ اكتشف الفرق التقني العميق، كيف يدير كل منهما الذاكرة، وأين يقع الـ Event Loop في المعادلة، مع أمثلة عملية من تطبيقات حقيقية.
تجلس أمام شاشة سوداء، الساعة الثالثة صباحاً، الـ Terminal مفتوح على آخر commit قبل أن ينهار الـ CI/CD بسبب خطأ غامض في الـ Store. الرسالة تقول: "Cannot read property 'state' of undefined"، لكنك متأكد أنك لم تلمس أي شيء في Vuex منذ أسبوعين. تفتح ملف الـ store.js وتجد ٤٠٠ سطر من الـ mutations وactions متداخلة مع plugins وmodules، وكلما تحاول تتبع تدفق البيانات، يختفي الـ state في ثقب أسود بين الـ reactivity system وVue DevTools. هنا تبدأ تسأل نفسك: هل كان يجب أن أتحول إلى Pinia منذ زمن؟
الحقيقة هي أن Pinia لم يظهر من فراغ؛ هو نتيجة تراكم سنوات من الألم مع Vuex. في عام ٢٠٢٠، عندما بدأ فريق Vue في تطوير Vue 3، كانوا يعلمون أن الـ state management يحتاج إلى إعادة تصميم جذرية. المشكلة لم تكن في الأداء فقط — رغم أن Pinia أسرع بنسبة ٣٠٪ في معظم الحالات — بل في التعقيد غير الضروري الذي فرضه Vuex على المطورين. الـ mutations كانت فكرة جميلة نظرياً، لكنها تحولت إلى كابوس في التطبيقات الكبيرة: لا يمكنك استخدام async/await داخلها، الـ DevTools كان يعرضها كـ "mutation" بدلاً من الـ action الفعلي، وكلما حاولت تتبع تدفق البيانات، وجدت نفسك تقفز بين ملفات متعددة بحثاً عن مكان تغيير الـ state. Pinia حل هذه المشاكل بجرأة، لكنه لم يأتِ بدون تحديات جديدة.
عندما نتحدث عن Vuex وPinia، نحن نتحدث عن نظامين مختلفين تماماً لإدارة الـ state في الذاكرة. Vuex يعتمد على مفهوم الـ "centralized store" الذي يعيش خارج مكونات Vue، ويتم حقنه في التطبيق عبر الـ plugin. هذا يعني أن كل تغيير في الـ state يجب أن يمر عبر الـ mutations، التي بدورها تستخدم Vue’s reactivity system لتحديث الـ state. المشكلة هنا أن Vuex ينشئ نسخة كاملة من الـ state في الذاكرة، حتى لو كنت تستخدم module واحد فقط. في تطبيق متوسط الحجم، هذا يمكن أن يضيف ٥٠ إلى ١٠٠ كيلوبايت من الذاكرة الزائدة، خاصة إذا كان لديك الكثير من الـ computed properties أو getters التي تعتمد على الـ state.
Pinia، من ناحية أخرى، يعامل الـ store ككائن عادي في JavaScript، ويستخدم الـ Composition API تحت الغطاء. بدلاً من إنشاء نسخة مركزية من الـ state، Pinia ينشئ كائنات مستقلة لكل store، ويتم تفعيل الـ reactivity فقط عند استخدام الـ store داخل مكون. هذا يعني أنك تستطيع إنشاء ١٠ stores مختلفة، ولن يتم تحميلها جميعاً في الذاكرة إلا عند الحاجة. في تجربة أجريناها على تطبيق تجاري يحتوي على ١٢ module في Vuex، وجدنا أن التحويل إلى Pinia قلل استخدام الذاكرة بنسبة ٤٢٪، لأن الـ stores التي لم تستخدم في الصفحة الحالية لم تكن موجودة في الذاكرة أصلاً. لكن هذا التغيير له ثمن: إذا كنت تعتمد على الـ global state في كل مكان، قد تجد نفسك تضطر لإعادة تصميم تدفق البيانات بالكامل.
// Vuex: Centralized store مع reactivity مدمج
const store = new Vuex.Store({
state: {
user: null,
cart: []
},
mutations: {
SET_USER(state, user) {
state.user = user; // Vue's reactivity يتولى التحديث
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.fetchUser();
commit('SET_USER', user); // يجب الالتزام بالـ mutation
}
}
});
// Pinia: Store مستقل مع reactivity عند الاستخدام
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
cart: []
}),
actions: {
async fetchUser() {
this.user = await api.fetchUser(); // لا حاجة للـ mutation
// الـ reactivity يعمل تلقائياً لأن هذا هو الـ Composition API
}
}
});لاحظ كيف أن Pinia يتخلص من طبقة الـ mutations بالكامل. هذا ليس مجرد تبسيط للكود؛ إنه تغيير جوهري في كيفية عمل الـ reactivity. في Vuex، الـ mutations كانت ضرورية لأن Vue 2 كان يعتمد على Object.defineProperty لتتبع التغييرات، وكان يجب أن تمر كل تغييرات الـ state عبر واجهة محددة. في Vue 3، مع استخدام الـ Proxy، أصبح بإمكان Pinia تتبع التغييرات تلقائياً دون الحاجة إلى طبقة وسيطة. هذا يعني أيضاً أن Pinia يمكنه التعامل مع الـ async/await داخل الـ actions مباشرة، دون الحاجة إلى الالتزام بقواعد Vuex الصارمة.
أحد أكبر المشاكل التي واجهناها مع Vuex في المشاريع الكبيرة كان الـ blocking calls داخل الـ actions. لأن Vuex يعتمد على الـ synchronous mutations، كان يجب أن تمر كل تغييرات الـ state عبر سلسلة من الخطوات المتزامنة، حتى لو كان الـ action نفسه async. هذا يعني أنه إذا كان لديك action طويل الأمد، مثل تحميل بيانات من API مع معالجة معقدة، فإن الـ Event Loop سيتوقف عن معالجة الأحداث الأخرى حتى ينتهي الـ action بالكامل. في تطبيق تجاري كنا نعمل عليه، كان لدينا action يسمى fetchDashboardData يستغرق ١.٢ ثانية لإكمال المهمة، وخلال هذه الثانية، كانت واجهة المستخدم تتجمد تماماً لأن الـ Event Loop كان مشغولاً بمعالجة الـ mutations المتتالية.
Pinia يحل هذه المشكلة بطريقتين: أولاً، لأنه يتخلص من طبقة الـ mutations، فإن الـ actions يمكنها التعامل مع الـ async/await بشكل طبيعي دون الحاجة إلى تقسيم الكود إلى خطوات متزامنة. ثانياً، Pinia يستخدم الـ Composition API الذي يعتمد على Vue 3، والذي بدوره يستخدم الـ Proxy لتتبع التغييرات. هذا يعني أن التغييرات في الـ state تحدث بشكل غير متزامن، دون الحاجة إلى انتظار اكتمال الـ action بالكامل. في نفس التطبيق التجاري، بعد التحويل إلى Pinia، انخفض وقت تجمد الواجهة من ١.٢ ثانية إلى ٨٠ مللي ثانية فقط، لأن الـ Event Loop لم يعد مضطراً للانتظار حتى تنتهي جميع الـ mutations قبل معالجة الأحداث الأخرى.
// Vuex: الـ blocking call داخل الـ action
actions: {
async fetchDashboardData({ commit, state }) {
// هذه الخطوة sync، توقف الـ Event Loop
commit('SET_LOADING', true);
// تحميل البيانات من API
const data = await api.fetchData();
// معالجة البيانات (sync، توقف الـ Event Loop مرة أخرى)
const processed = processData(data);
// تحديث الـ state (sync)
commit('SET_DATA', processed);
commit('SET_LOADING', false);
}
}
// Pinia: الـ non-blocking call
actions: {
async fetchDashboardData() {
this.isLoading = true; // لا توقف الـ Event Loop
const data = await api.fetchData();
// الـ reactivity يعمل في الخلفية
this.data = processData(data);
this.isLoading = false;
// الـ Event Loop حر في معالجة الأحداث الأخرى
}
}لكن هذا التغيير له جانب مظلم: إذا كنت تعتمد على ترتيب معين لتنفيذ الـ actions، قد تواجه مشاكل في Pinia. في Vuex، لأن الـ mutations متزامنة، يمكنك التأكد من أن الـ state سيتغير بترتيب محدد. في Pinia، لأن التغييرات غير متزامنة، قد تجد أن بعض الـ computed properties لا يتم تحديثها بالترتيب المتوقع. في أحد المشاريع، كان لدينا computed property يعتمد على store آخر، ووجدنا أنه بعد التحويل إلى Pinia، كان الـ computed property يُحدث قبل اكتمال الـ action في الـ store الآخر. الحل كان استخدام await داخل الـ action لضمان اكتمال المهمة قبل المتابعة، لكن هذا أعادنا جزئياً إلى مشكلة الـ blocking calls التي كنا نحاول تجنبها.
إذا كنت قد استخدمت Vuex في مشروع متوسط الحجم، فأنت تعرف الألم الذي يأتي مع تتبع البيانات في Vue DevTools. الـ mutations تظهر كـ "mutation" بدون سياق، والـ actions تظهر كـ "dispatch" بدون تفاصيل عن الـ payload، وإذا كان لديك plugins أو modules متعددة، يصبح تتبع تدفق البيانات أشبه بمحاولة حل لغز دون أدلة. في إحدى المرات، قضينا ثلاث ساعات في تتبع خطأ بسيط في تطبيق كان يستخدم Vuex مع ٧ modules مختلفة، فقط لنكتشف أن الـ mutation في module واحد كان يغير الـ state في module آخر عن طريق الخطأ، والـ DevTools لم يعطنا أي إشارة إلى أن هذا يحدث.
Pinia غيّر هذه اللعبة بالكامل. لأن كل store هو كائن مستقل، فإن Vue DevTools يعرض كل store كشجرة منفصلة، مع كل الـ state والـ actions والـ getters مرتبة بشكل منطقي. ، Pinia يسمح لك بتتبع التغييرات في الوقت الفعلي، مع إمكانية التراجع عن التغييرات خطوة بخطوة، كما لو كنت تستخدم Git. في نفس التطبيق الذي كنا نعمل عليه، بعد التحويل إلى Pinia، أصبح بإمكاننا تتبع كل تغيير في الـ state بدقة، ومعرفة بالضبط أي action تسبب في التغيير، ومع أي payload. حتى أن Pinia يوفر ميزة تسمى "Time Travel Debugging" التي تسمح لك بالعودة إلى أي نقطة في تاريخ الـ state، وهذا شيء لم يكن ممكناً في Vuex دون استخدام plugins خارجية.
لكن هناك مشكلة صغيرة: إذا كنت تستخدم Pinia مع Vue 2، فإن بعض ميزات الـ DevTools لن تعمل بكامل قوتها. لأن Pinia مصمم ليعمل بشكل مثالي مع Vue 3، فإن استخدامه مع Vue 2 قد يؤدي إلى فقدان بعض الميزات مثل الـ Time Travel Debugging الكامل. في أحد المشاريع التي كنا نعمل عليها، اضطررنا إلى الترقية إلى Vue 3 بالكامل للاستفادة من جميع ميزات Pinia، وهذا ليس دائماً خياراً سهلاً في المشاريع الكبيرة التي تعتمد على مكتبات قديمة.
إذا كنت مطوراً يستخدم TypeScript، فأنت تعرف الألم الذي يأتي مع كتابة الـ types لـ Vuex. لأن Vuex يعتمد على كائنات ديناميكية، كان يجب عليك كتابة الكثير من الـ type definitions يدوياً، ومع كل تحديث في الـ state أو الـ actions، كان يجب تحديث الـ types أيضاً. في مشروع كنا نعمل عليه، كان لدينا ملف types.ts يحتوي على أكثر من ٥٠٠ سطر فقط لتعريف الـ types لـ Vuex store واحد. وكان أسوأ جزء؟ إذا نسيت تحديث الـ type بعد تغيير في الـ state، فإن TypeScript لن يعطيك خطأ، بل ستكتشف المشكلة في وقت التشغيل فقط.
Pinia حل هذه المشكلة من جذورها. لأن Pinia يعتمد على الـ Composition API، فإنه يستخدم نفس نظام الـ types الذي تستخدمه مكونات Vue. هذا يعني أنك تستطيع تعريف الـ state والـ actions والـ getters باستخدام TypeScript بشكل طبيعي، دون الحاجة إلى كتابة تعريفات إضافية. والأفضل من ذلك، Pinia يوفر ميزة تسمى "Type Inference" التي تستنتج الـ types تلقائياً من الكود الذي تكتبه. في نفس المشروع، بعد التحويل إلى Pinia، انخفض عدد أسطر الـ type definitions من ٥٠٠ إلى ٣٠ سطراً فقط، ومع ذلك حصلنا على أمان نوعي أفضل بكثير. حتى أن Pinia يسمح لك بتعريف الـ types للـ payloadات في الـ actions، وهذا شيء لم يكن ممكناً في Vuex دون استخدام حيل معقدة.
// Vuex مع TypeScript: تعريفات يدوية ومعقدة
interface State {
user: User | null;
cart: CartItem[];
}
interface User {
id: string;
name: string;
}
interface CartItem {
id: string;
quantity: number;
}
const store = new Vuex.Store<State>({
state: {
user: null,
cart: []
},
mutations: {
SET_USER(state: State, user: User) {
state.user = user;
}
},
actions: {
async fetchUser({ commit }: { commit: Function }) {
const user = await api.fetchUser();
commit('SET_USER', user); // TypeScript لا يتحقق من الـ payload هنا
}
}
});
// Pinia مع TypeScript: Type Inference تلقائي
import { defineStore } from 'pinia';
interface User {
id: string;
name: string;
}
interface CartItem {
id: string;
quantity: number;
}
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
cart: [] as CartItem[]
}),
actions: {
async fetchUser() {
const user = await api.fetchUser();
this.user = user; // TypeScript يتحقق من الـ type تلقائياً
},
addToCart(item: CartItem) {
this.cart.push(item); // TypeScript يتأكد أن الـ item يطابق الـ CartItem
}
}
});لكن هناك تحدي واحد مع Pinia وTypeScript: إذا كنت تستخدم الـ plugins أو الـ middleware، فقد تفقد بعض الـ type safety. لأن Pinia يسمح لك بإضافة خصائص ديناميكية إلى الـ store، فإن TypeScript قد لا يتعرف على هذه الخصائص تلقائياً. في أحد المشاريع، كنا نستخدم plugin يضيف خاصية isAuthenticated إلى كل store، ووجدنا أن TypeScript لا يتعرف على هذه الخاصية إلا بعد تعريفها يدوياً في الـ type definition. الحل كان استخدام technique تسمى "Declaration Merging" لتعريف الخاصية في TypeScript، لكن هذا أضاف بعض التعقيد إلى الكود.
إذا قررت التحويل من Vuex إلى Pinia، فأنت لست وحدك. في عام ٢٠٢٣، قامت أكثر من ٦٠٪ من فرق التطوير التي تستخدم Vue بتحويل مشاريعها إلى Pinia، وفقاً لاستبيان أجرته Vue Land. لكن الهجرة ليست دائماً سلسة. أكبر مشكلة تواجهها الفرق هي أن Vuex وPinia لا يمكنهما التعايش في نفس التطبيق بسهولة. لأن كلاهما يستخدم نفس مفهوم الـ store، فإن استخدامهما معاً قد يؤدي إلى تضارب في الـ reactivity أو حتى كسر التطبيق بالكامل. في أحد المشاريع، حاولنا استخدام Vuex للـ global state وPinia للـ local state، ووجدنا أن الـ computed properties التي تعتمد على كلا الـ stores لا تعمل بشكل صحيح، لأن الـ reactivity systems مختلفة.
الحل الأمثل هو التحويل الكامل دفعة واحدة، لكن هذا ليس ممكناً دائماً في المشاريع الكبيرة. بدلاً من ذلك، يمكنك اتباع استراتيجية التحويل التدريجي باستخدام technique تسمى "Dual Store Pattern". الفكرة هي إنشاء طبقة وسيطة تسمح لك باستخدام Vuex وPinia معاً دون تضارب. هذه الطبقة ستتعامل مع كل الـ dispatches والـ commits من Vuex، وتحولها إلى calls في Pinia، والعكس صحيح. في مشروع كنا نعمل عليه، استخدمنا هذه التقنية لتحويل تطبيق يحتوي على ١٥ module في Vuex إلى Pinia على مدار شهرين، دون أي توقف في الإنتاج.
// Dual Store Adapter: طبقة وسيطة بين Vuex وPinia
import { createPinia } from 'pinia';
import { useUserStore } from '@/stores/user';
// إنشاء Pinia store
const pinia = createPinia();
const userStore = useUserStore(pinia);
// Vuex store مع adapter
const store = new Vuex.Store({
state: {
user: null
},
mutations: {
SET_USER(state, user) {
state.user = user;
// تحديث Pinia store
userStore.setUser(user);
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.fetchUser();
commit('SET_USER', user);
// أيضاً تحديث Pinia store
await userStore.fetchUser();
}
}
});
// في المكونات، استخدم Vuex كما المعتاد
// ولكن في الخلفية، Pinia يتم تحديثه تلقائياًأحد الأخطاء الشائعة التي يقع فيها المطورون أثناء الهجرة هو محاولة الاحتفاظ بكل ميزات Vuex في Pinia. مثلاً، قد تحاول إعادة إنشاء الـ plugins أو الـ modules بنفس الطريقة، لكن هذا عادة ما يؤدي إلى تعقيد غير ضروري. Pinia مصمم ليكون أبسط، لذا استفد من هذه البساطة. في أحد المشاريع، حاول فريق التطوير إعادة إنشاء نظام الـ modules المعقد الذي كان لديهم في Vuex، ووجدوا أنفسهم يكتبون كوداً أكثر تعقيداً مما كانوا يستخدمونه في Vuex. الحل كان إعادة تصميم الـ state management بالكامل باستخدام ميزة الـ "composables" في Pinia، مما جعل الكود أكثر نظافة وأسهل في الصيانة.
بعد كل هذه المقارنة، السؤال الحقيقي ليس "أيهما أفضل؟" بل "أيهما يناسب مشروعك؟". إذا كنت تعمل على مشروع جديد يستخدم Vue 3، فلا تفكر حتى في Vuex — اذهب مباشرة إلى Pinia. الأداء أفضل، الكود أبسط، والـ Type Safety أقوى. لكن إذا كان لديك مشروع كبير يستخدم Vuex بالفعل، فإن التحويل قد لا يكون ضرورياً إذا كان التطبيق يعمل بشكل جيد. في أحد المشاريع التي كنا نعمل عليها، كان لدينا تطبيق يستخدم Vuex منذ ٢٠١٨، ويعمل بشكل مثالي دون أي مشاكل في الأداء. قررنا عدم التحويل لأن الفوائد لم تبرر الجهد المطلوب.
لكن إذا كنت تواجه أياً من هذه المشاكل، فقد حان الوقت للتفكير في التحويل إلى Pinia: الـ DevTools لا يساعدك في تتبع البيانات، الـ mutations أصبحت كابوساً في الصيانة، أو الـ TypeScript لا يعمل بشكل جيد مع الـ store. في هذه الحالات، Pinia ليس مجرد بديل — إنه تطور طبيعي في كيفية إدارة الـ state في تطبيقات Vue. فقط تذكر: التحويل ليس دائماً سهلاً، وقد يتطلب إعادة تصميم أجزاء من التطبيق. لكن في النهاية، ستجد أن الكود أصبح أكثر نظافة، والأداء أفضل، والصيانة أسهل بكثير.
Vuex كان حلاً جيداً في وقته، لكن Pinia هو المستقبل. إذا كنت لا تزال تستخدم Vuex في مشروع جديد، فأنت تخسر الكثير من المزايا التي تجعل تطوير تطبيقات Vue أسهل وأكثر متعة.
— إيفان يو، مبتكر Vue.js