Vuex ile Vue.js State Yönetimi
Modern JavaScript frameworkleri ile uygulama geliştirirken birçok component (bileşen) kullanmaktayız. Bunların kendi içerisinde barındırdığı (child) ya da içerisinde bulunduğu (parent) componentler arası iletişim kurma ya da veri alışverişi gibi birçok işlemler gerçekleştiririz. Uygulamalar büyüdükçe, component sayısı arttıkça bu tür işlemler daha da karmaşık hale gelmektedir. Bu yazıda Vue.js framework’ü için state (durum) yönetimini merkezi bir yerden yönetmemizi sağlayan Vuex hakkında bilgiler edineceğiz.
Vuex Nedir?
Vuex, Vue.js state’ini merkezi olarak yönetmeye yarayan open-source bir kütüphanedir. Vue.js geliştiricileri tarafından geliştirilmektedir.
Peki neden Vuex’e ihtiyaç duyarız? Şöyle bir kod örneğinden konuyu ele alalım.
Yukarıdaki görseldeki kod baktığımız zaman; state değerini kendi içerisinde barındıran bir Vue instance (örneği) bulunmaktadır.
- State: uygulama içerisindeki kullanılacak olan veri (count).
- View: State içerisindeki verilerin gösterileceği alan (template).
- Action: Kullanıcıdan gelecek herhangi bir olaya karşılık state’in değiştirilmesini gerçekleştiren yapı (increment).
Yukarıdaki kodu şekil üzerinde modellediğimiz zaman aşağıdaki gibi bir görsel oluşmaktadır. Bu görselde de tek yönlü bir veri akışı söz konusudur. (one-way data flow)
Basit bir yapı fakat aynı state’i kullanan birden fazla component olduğu zaman bu basitlik kırılgan hale gelmektedir.
- Birden fazla View, aynı state’i kullanmak isteyebilir.
- Birden fazla Action, aynı state üzerinde işlem yapmak isteyebilir.
Yukarıdaki problemleri çözmek için yollar mevcut fakat bunları gerçekleştirmek işin basitliğini bozmakta ve uygulama iyice karışık hale gelmektedir.
Örneğin: Birinci probleminin çözümü için iç içe componentler oluşturulabilir ve state bir alt componentlere props olarak gönderilebilir fakat iç içe olan yapılara sürekli props gönderilmesi ve alt componentlerde değiştirilen props değerlerinin üst component’e etki etmemesi gibi durumlar esnek bir çözüm sağlamamaktadır.
Bu ve bunun gibi senaryolar state yönetiminin merkezi bir hale getirilmesine doğru gitmiştir; state’in componentlerden ayrılması, singleton (tekil) olarak ayarlanması ve yönetilmesi. Bu sayede hangi component içerisinde olunursa olunsun ilgili state’e erişilebilir ve herhangi bir Action tetiklenebilir ve hepsi de aynı state üzerinde işlem gerçekleştirmiş olur.
Vuex’in bizlere sağladığı da bu. State’i, bu state’i değiştirecek yapıları uygulama içerisinden ayırarak merkezi bir yerden yönetilmesini sağlamak.
Vuex Mimarisi ve Core Konseptler
Vuex, bakım ve geliştirme kolaylığından dolayı state’i ve bu state üzerinde işlem yapacak yapıları merkezi bir yerde toplamıştır.
Yukarıdaki görselde görülen bütün durumlar Vuex içerisindeki store’da kayıtlıdır. Store, basit anlamda uygulamanın state’ini barındıran yapıdır.
Her ne kadar Vuex’e ait store’un global bir obje olduğunu söylesek de onu global objelerden ayıran temel 2 olay vardır.
- Vuex store’u reactivity (tepkisel, çeviri olmayan halini bilmek daha sağlıklı) bir yapıdadır. Store içerisindeki state içerisinde herhangi bir değişiklik olduğunda componentler uyarılacak ve efektif bir şekilde güncellencektir.
- Direkt olarak store içerisindeki state’i değiştirememekteyiz. Bunu Commit yardımı ile (yazının ilerleyen kısımlarında değineceğiz.) açıkça belirtmek gerekiyor.
Vuex; Actions, Mutations, State, Getters olmak üzere temel 4 bölümden oluşmaktadır. Bunları daha detaylı görmek için temel bir uygulama üzerinden gideceğiz.
vue-cli yardımı ile boş bir proje oluşturalım:
vue create vuex-example
Başlangıç için Vuex implementasyonunu göstereceğimizden dolayı “Default ([Vue 2] babel, eslint)” seçeneğini seçelim.
npm ile Vuex’in indirilmesi:
npm install vuex –-save
Vuex’in implementasyonu oldukça basit. Yapmamız gereken işlemler sırasıyla;
- store klasörü altında index.js adında bir JavaScript dosyası oluşturuyoruz. İçerisinde Vue’nin Vuex’i kullanması gerektiğini söylüyoruz daha sonra ise yeni bir Store instance oluşturup ilgili alanları (state, getters, actions, mutations) tanımlıyoruz ve export ediyoruz.
- js içerisinde bütün componentlerin store’a erişmesi için Vue içerisine ekliyoruz.
Yukarıda temel anlamda bir Vuex Store objesi oluşturduk ve main.js içerisinde kullanılmak üzere export ettik. Export edilen bu store değerinin main.js içerisinde kayıt edilmesi işlemi de aşağıdaki gibidir;
Temel implementasyon bu şekilde.
Vuex’e ait store içerisinde; state, getters, mutations, actions gibi alanların varlığından bahsettik ve implementasyon kısmında da boş olarak tanımlamasını yaptık. Bunların detaylarına bakacak olursak;
State
Vuex’in singleton bir state tuttuğundan bahsettik. Uygulama içerisindeki bütün yapılar aynı state’i kullanır. Uygulama içerisindeki state verileri burada tutulur.
State içerisinde tanımlanan değerlere component içerisinde erişmenin birçok yolu mevcut.
<template> <div id="app"> {{ count }} </div> </template> <script> export default { computed: { count() { return this.$store.state.count; }, }, }; </script>
Daha öncesinde store’u Vue içerisinde bind (bağlama) işlemini gerçekleştirdik ve store’un state’i barındırdığından bahsettik. Yukarıdaki koda baktığımız zaman, state değerine erişmek için $store
kullanıyoruz ve Vue instance içerisinde yer almaktadır. $store.state.count
ile state içerisindeki count verisine rahatlıkla erişebiliyoruz. Bu işlem direkt template içerisinde de yapılabilirdi fakat computed içerisinde yapılması daha sağlıklı olacaktır. State’in reactivity’sinden dolayı herhangi bir değişiklik olduğunda computed tetiklenecek ve ilgili alanlar tekrardan güncellenecektir.
State üzerindeki veri sayısı arttıkça ilgili dataları computed içerisine koymak can sıkıcı ve kod kalabalığına neden olabiliyor bu gibi problemler için Vuex içerisinde gelen mapState
ile otomatik bir şekilde gerçekleştirebiliyoruz.
<script> import { mapState } from "vuex"; export default { data() { return { localCount: 5, }; }, computed: mapState({ // kısa bir şekilde tanımlamak için arrow function kullanılabilir. count: (state) => state.count, // ilgili state adına alias verilebilir, count için `state => state.count` ifade denk gelmektedir. countAlias: "count", // Normal fonksiyon kullanılarak ilgili local değişkene de this anahtar kelimesi ile erişilebilir. countPlusLocalState(state) { return state.count + this.localCount; }, }), }; </script>
mapState
içerisindeki tanımları yaparken array olarak da geçebiliriz, tıpkı aşağıdaki gibi.
computed: mapState([ // state.count içerisindeki değere artık this.count diyerek erişebiliyoruz. "count", ])
Uzun uzun this.$store.state.count
yazmak ya da bind etmek ile uğraşmak yerine this.count
yazarak da state içerisindeki o veriye erişebiliyoruz.
Yukarıdaki kod örneğine bakıldığında computed içerisine direkt mapState’in geriye döndüğü objeyi atadık. Özel olarak kendi computed propertylerimizi tanımlamak yanına da state içerisindeki verileri kullanmak istiyorsak spread operatörile bu işlemleri gerçekleştirebiliyoruz.
computed: { // Özel olarak local computed oluşturabiliyoruz. localComputed() { /* ... */ }, // Store içerisindeki state'i tanımlayabiliyoruz. ...mapState({ /* ... */ }), }
Getters
Getter dediğimiz yapı Vue içerisindeki computed property’e benzerdir.
Uygulama içerisindeki state içerisindeki bir datayı belirli işlemlerden geçirmek istediğimizde, örneği filtrelemek. Aşağıdaki gibi bir işlem gerçekleştirebiliriz.
computed: { doneTodosCount() { return this.$store.state.todos.filter((todo) => todo.done).length; }, },
Yukarıdaki koda baktığımız zaman state içerisindeki todos arrayinde tamamlananların sayısı getirilmekte fakat filtreleme işleminin sonucunu birkaç component içerisinde kullanmak istediğimizde yukarıdaki kodu kopyalayıp diğer componentler içerisine de koymamız gerekmektedir. Bu tür senaryolarda Vuex’in sağladığı Getter yapısından faydalanılmaktadır.
Getter tanımına baktığımızda 2 argüman almaktadır ve bu argümanlardan birincisi state değeridir ikincisi ise diğer getterların bulunduğu getters idir.
Aşağıda store/index.js
içerisine tanımlanan state
ve getters
görülmektedir.
export const store = new Vuex.Store({ state: { todos: [ { id: 1, text: "...", done: true }, { id: 2, text: "...", done: false }, ], }, getters: { doneTodosCount: (state, getters) => { return state.todos.filter((todo) => todo.done).length; }, }, });
İlgili getter’in component içerisinde çağrılması için tıpkı state’te olduğu gibi benzer yolları vardır.
Component (template) içerisinde Store üzerinden direkt erişim için:
this.$store.getters.doneTodosCount; // -> 1
Computed içerisinde kullanımı:
computed: { doneTodosCount() { return this.$store.getters.doneTodosCount; }, },
Tıpkı State içerisinde mapState
kullanarak yaptığımız pratik mapleme işlemini Getters için de yapabilmekteyiz. Bunun için ise mapGetters
’dan faydalanmaktayız.
<template> <div id="app"> {{ doneCount }} </div> </template> <script> import { mapGetters } from "vuex"; export default { computed: { ...mapGetters({ // map `this.doneCount` to `this.$store.getters.doneTodosCount` doneCount: "doneTodosCount", }), }, }; </script>
Yukarıda ilgili Getter’a alias verildi. Direkt olarak maplemek istersek de aşağıdaki gibi kullanabiliriz.
<template> <div id="app"> {{ doneTodosCount }} </div> </template> <script> import { mapGetters } from "vuex"; export default { computed: { ...mapGetters(["doneTodosCount", "anotherGetter"]), }, }; </script>
Mutations
Mutationları denilen yapıları state içerisindeki verileri güncellerken kullanmaktayız. Buradaki her mutation type ve handler olmak üzere 2 yapı içermektedir. Type dediğimiz alan metot ismi, handler ise ilgili state üzerinde güncelleme işlemini yapacak metottur. Bu metot 2 parametre almaktadır ve ilk parametresi state diğer parametre ise data.
Aşağıda store/index.js
içerisine tanımlanan state
ve mutations
görülmektedir.
export const store = new Vuex.Store({ state: { count: 0, }, mutations: { increment(state) { // mutate state state.count++; }, }, });
Yukarıda bir mutation görülmektedir ve yaptığı işlem state içerisindeki count’u bir artırmak. State ve Getters içerisinde olduğu gibi direkt erişme durumu maalesef Mutations içerisinde bulunmamaktadır. Bunu gerçekleştirmek için commit ile bildirmek gerekiyor.
Vue component’i içerisinden erişim aşağıdaki gibidir.
this.$store.commit("increment");
Yukarıda bir mutation’un tetiklenmesi gerçekleştirildi, eğer parametre olarak data da gönderilmek isteniyorsa 2. parametre olarak gönderilmektedir.
export const store = new Vuex.Store({ state: { count: 0, }, mutations: { increment(state, payload) { // mutate state state.count += payload.amount; }, }, });
Vuex dökümantasyonu gönderilen payload’ın obje olarak gönderilmesini önermektedir.
this.$store.commit("increment", { amount: 4 });
Component içerisinde pratik mapleme işlemlerini gerçekleştirmek için mapMutations
’tan yararlanmaktayız.
<template> <div id="app"> <h1> {{ this.$store.state.count }} </h1> <button @click="increment">Up </div> </template> <script> import { mapMutations } from "vuex"; export default { methods: { ...mapMutations([ "increment", // map `this.increment()` to `this.$store.commit('increment')` // `mapMutations` payload’ı desteklemektedir: "incrementBy", // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: "increment", // map `this.add()` to `this.$store.commit('increment')` }), }, }; </script>
Yukarıda görüldüğü gibi direkt array olarak aynı isimde de alabiliyoruz alias vererek de.
Actions
Actions ile Mutations benzer yapılardır fakat aralarında önemli farklar bulunmaktadır. Bu farktan dolayı kullanım yeri oldukça önemlidir.
En önemli fark; action asenkron çalışmayı destekler. Genellikle API çağrılarında kullanılır.
Aşağıda store/index.js
içerisine tanımlanan state
, mutations
ve actions
görülmektedir.
export const store = new Vuex.Store({ state: { todos: [], }, mutations: { insertTodos(state, payload) { state.todos = payload; }, }, actions: { fetchTodos(context) { fetch("https://jsonplaceholder.typicode.com/todos") .then((response) => response.json()) .then((data) => { context.commit("insertTodos", data); }); }, }, });
Yukarıdaki örnekte fetchTodos
adında bir metot tanımlı ve ilgili yere istek atarak todo listesini alıyor ve state’i güncellemesi için de mutation’ı tetiklemektedir. Bu sayede Action ile gelen veri Mutation ile ilgili State’i güncelleyecektir ve component update olarak ilgili alanlar güncellencektir.
Action içerisine tanımlanan metotlar context
adında bir parametre almaktadır. Context kendi içerisinde; state, getters, commit, dispatch gibi özellikleri barındırmaktadır. Duruma göre sürece uygun olan işlem kullanılabilir.
Tanımlanan action’un component içerisindeki çağrımı dispatch
işlemi ile gerçekleştirilir.
<script> export default { created() { this.$store.dispatch("fetchTodos"); }, }; </script>
Yukarıda birçok kavrama değindik, işin sürecini özetlemek gerekirse:
- İlgili action dispatch ile tetiklenir, API isteğinde bulunulur ve data alınır.
- Action içerisinde gelen data ile state içerisindeki değeri güncellemek için Mutation’dan yararlanılır, commit atılır.
- İlgili Mutation state değerini günceller ve o state’i kullanan Getter tetiklenir ve o Getter’i kullanan component update olur.
Bunları içeren toplu bir örnek vermek gerekirse de
Aşağıda store/index.js
içerisine tanımlanan state
, getters
, mutations
ve actions
görülmektedir.
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export const store = new Vuex.Store({ state: { todos: [], }, getters: { getCompletedTodos(state) { return state.todos.filter((todo) => todo.completed); }, }, mutations: { insertTodos(state, payload) { state.todos = payload; }, }, actions: { fetchTodos(context) { fetch("https://jsonplaceholder.typicode.com/todos") .then((response) => response.json()) .then((data) => { context.commit("insertTodos", data); }); }, }, });
Yukarıda fetchTodos
adında bir action bulunmaktadır, API isteği ile datayı alarak ilgili mutation’ı commit ile tetikliyor, buradaki metotumuz insertTodos
. Mutation ise state’i güncelliyor ve bu güncellemeden dolayı getCompletedTodos
Getter’ını kullanan componentler update olarak ilgili güncel datayı kullanır.
<template> <div id="app"> <ul> <li v-for="todo in getCompletedTodos" :key="todo.id"> {{ todo.title }} </li> </ul> </div> </template> <script> import { mapActions, mapGetters } from "vuex"; export default { methods: { ...mapActions(["fetchTodos"]), }, computed: { ...mapGetters(["getCompletedTodos"]), }, created() { this.fetchTodos(); }, }; </script>
Yukarıda ise ilgili işlemlerin maplenip kullanılması ve listelenmesi yer almaktadır.
Buraya kadar Vuex’in hangi bileşenlerden oluştuğunu, ne gibi kolaylıklar sağladığını ve nasıl kullanıldığını öğrendik.
State yönetimi işleminin daha okunulabilir, sürdürülebilir (modüler yapıya geçiş) ve diğer detaylar hakkında kendi resmi dökümantasyonunda daha fazla bilgi yer almaktadır.
Kaynaklar: