Vuex ile Vue uygulamasında durum yönetimi
Vuex ile bileşenler arası paylaşılan durumu tek bir kaynakta toplamak; state, mutation, action ve getter kavramları.
Vue ile bileşen tabanlı arayüz yazmaya başladığımda bir süre $emit ve props ile idare ettim. Küçük uygulamalarda bu yeterli. Ama bileşen sayısı artınca ve birbirleriyle ilişkisi olmayan kardeş bileşenler aynı veriyi paylaşmak zorunda kalınca bu yol çıkmaza giriyor. Kimin kime ne gönderdiğini takip etmek güçleşiyor.
Vuex, Vue için geliştirilen merkezi state management kütüphanesi. Paylaşılan veriyi tek bir yerde toplamak ve tüm bileşenlerin aynı kaynaktan okuyup aynı kurallara göre güncellemesini sağlamak için tasarlanmış.
Temel yapı
Vuex’in dört ana kavramı var:
- State: Uygulamanın tek doğru kaynağı. Tüm bileşenler buradan okur.
- Mutation: State’i değiştirmenin tek yolu. Senkron olmalıdır.
- Action: Asenkron işlemleri barındırır; bitince mutation çağırır.
- Getter: State’ten türetilmiş, hesaplanmış değerler.
Basit bir alışveriş sepeti örneği:
const store = new Vuex.Store({
state: {
cartItems: []
},
mutations: {
ADD_ITEM(state, item) {
state.cartItems.push(item);
},
REMOVE_ITEM(state, itemId) {
state.cartItems = state.cartItems.filter(i => i.id !== itemId);
}
},
actions: {
addToCart({ commit }, item) {
// Asenkron bir kontrol yapılabilir; örn. stok sorgusu
commit('ADD_ITEM', item);
}
},
getters: {
cartTotal(state) {
return state.cartItems.reduce((sum, item) => sum + item.price, 0);
}
}
});
Bileşenden store’a erişim
Store Vue örneğine enjekte edildiğinde tüm bileşenlerden this.$store ile erişilebilir:
// Ürün bileşeni
methods: {
addProduct(product) {
this.$store.dispatch('addToCart', product);
}
},
computed: {
total() {
return this.$store.getters.cartTotal;
}
}
Sepet bileşeni ve ürün listesi bileşeni tamamen farklı yerlerde olabilir; ikisi de aynı store’dan okuduğu için senkronizasyon kendiliğinden sağlanıyor.
mapState ve mapGetters
Her seferinde this.$store.state.cartItems yazmak yorucu. Vuex’in yardımcı fonksiyonları bunu kısaltıyor:
import { mapState, mapGetters } from 'vuex';
computed: {
...mapState(['cartItems']),
...mapGetters(['cartTotal'])
}
Bu sayede this.cartItems ve this.cartTotal direkt kullanılabilir hale geliyor.
Mutation ve action arasındaki fark
Bu ayrımı ilk gördüğümde gereksiz gibi geldi: neden hem mutation hem action? Cevabı asenkron işlemlerde yatıyor.
Mutation daima senkron çalışmalıdır. Bu kural Vuex DevTools’un state değişikliklerini doğru sırayla kaydetmesini sağlıyor. Senkron olmayan bir mutation, araçların ne zaman ne değiştiğini takip etmesini zorlaştırır.
Action ise asenkron işleri üstlenir. API çağrısı yapılır, sonuç gelir, mutation commit edilir:
actions: {
async fetchCart({ commit }, userId) {
const response = await fetch(`/api/cart/${userId}`);
const items = await response.json();
commit('SET_CART', items);
}
}
Bu ayrım biraz fazla katı görünebilir; ama zamanla takdir ediyorsunuz. State değişikliği her zaman mutation üzerinden geçtiği için, her değişikliğin neden ve ne zaman olduğunu DevTools’ta görmek mümkün oluyor.
Ne zaman Vuex gerekiyor?
Her Vue projesine Vuex eklemek gerekmiyor. Kısa vadeli, birkaç bileşenden oluşan bir form arayüzü için props ve $emit yeterli. Şu koşullar varsa Vuex düşünmeye değer:
- Birden fazla, birbirine bağlı olmayan bileşen aynı veriyi okuyor.
- Sunucudan çekilen veri uygulamanın birçok yerinde kullanılıyor.
- Bir kullanıcı eylemi birden çok bileşeni etkiliyor.
Bu sorulardan ikisi veya üçü “evet” ise, store kurmak başlangıçta biraz ek iş gerektiriyor ama zamanla kazancını veriyor.
Bir uyarı: store’u erken ve her şey için kullanmaya başlamak da bir tuzak. Yerel bir bileşen state’i olarak kalması gereken şeyi (örneğin bir formun isLoading durumu) store’a taşımak kodu gereksiz yere karmaşıklaştırıyor. Kural basit: veri yalnızca bir bileşeni ilgilendiriyorsa yerel bırakın, birden fazlasını ilgilendiriyorsa store’a taşıyın.
Modüller ile store’u bölmek
Uygulama büyüdükçe tek bir store dosyası şişiyor. Vuex modülleri ile store parçalara ayrılabiliyor:
const cartModule = {
namespaced: true,
state: { items: [] },
mutations: { /* ... */ },
actions: { /* ... */ }
};
const store = new Vuex.Store({
modules: {
cart: cartModule,
user: userModule
}
});
namespaced: true ile erişim this.$store.dispatch('cart/addToCart', item) biçimine dönüşüyor. Bu ayarı açmadan modüller birbirinin mutation isimlerini ezebiliyor — nadir ama sinir bozucu bir hata.
Vuex DevTools ile izleme
Vue DevTools tarayıcı eklentisi Vuex ile birlikte çok daha kullanışlı hale geliyor. Her mutation’ın ne zaman, hangi veriyle tetiklendiğini time-travel debugging ile görmek mümkün. Arayüzdeki tuhaf davranışların kaynağını bulmak bu araçla gerçekten kolaylaşıyor.
Vuex’i ilk kullandığımda biraz aşırı mühendislik gibi geldi. Sonradan tek bir cartItems dizisinin beş farklı bileşene $emit zinciriyle ulaşmaya çalıştığım kodu hatırladım — ve store’un neden var olduğunu anladım.