Skip to content

VueCore – Thay thế Vuex bằng Composition API

Vuex là một thư viện hỗ trợ cho việc quản lý state (data) của VueJs.

Với Vuejs, việc giao tiếp giữa component dần trở nên phức tạp khi data của ta ngày càng nhiều hơn.
Nhất là khi giao tiếp giữa những component cùng cấp với nhau. Ta phải sử dụng props để truyền data thông qua một trung gian (parent component)
Chính vì thế việc ra đời của Vuex đã xóa nhòa sự phức tạp này.
Tóm lại . . . Sự ra đời của Vuex là để làm cho cuộc đời của ta trở nên dễ thở hơn NHƯNG . . .

Nhưng sao ?
Nhưng Vuex không thực sự làm ta dễ thở

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

Hố lỳ shiet ! Cái gì zậy hả bây bi à ?

Những gì mà Vuex đem lại cho ta là quá nhiều ! Nhưng trong nhiều trường hợp thì chưa cần thiết

Sự ra đời của Vue.observable ở version 2.6 đã mang lại một cuộc cách mạng về cách ta share state. Và sau đó, Vue 3 đã cho ra đời Composition API đã nâng trò chơi này lên một tầm cao mới

Đọc xong bài này ta sẽ biết cách hoàn mỹ nhất để sử dụng Composition Api as State holder và so sánh nó với Vuex

Bài viết này được dịch từ State Management with Composition API.

Sau khi đọc bài viết này tôi phải lưu lại nó ngay lập tức bằng cách dịch bài này. Big shoutout to Filip Rakowski vì đã viết bài này <3


Contents

    Quên Vuex đi !

    Composition API đã cho chúng ta một cách nhìn mới về global & local state. Bới vì nó không cần một “hosting component” (Vue instance v.v.) để chứa state mà bây giờ state có thể hoàn toàn đi chung với “business logic” . . . xíu nữa bạn sẽ hiểu :))

    Do đó ta có thể xây dựng hệ thống bằng những thành phần độc lập và không bị phụ thuộc

    Replicate Vuex bằng Composition API

    Vuex Store gồm 4 thành phần chính :

    • state
    • mutations
    • actions
    • getters

    state là phần đơn giản nhất để replicate. Chúng tôi chỉ cần tạo một thuộc tính reactive với ref

    *replicate = fake lại

    const state = ref({ post: {} })

    Tiếp theo là replicate mutation

    function setPost(post) { 
      state.value.post = post
    }

    Và bây giờ là action để xử lý tác vụ bất đồng bộ (async)

    async function loadPost(id) {
      const post = await fetchPost(id)
      setPost(post)
    }

    Cuối cùng là getter

    const getPost = computed(() => state.value.post)

    Voila! Và xem ta có gì nè

    const state = ref({ post: {} })
    
    function setPost(post) { 
      state.value.post = post
    }
    
    async function loadPost(id) {
      const post = await fetchPost(id)
      setPost(post)
    }
    
    const getPost = computed(() => state.value.post)
    
    export {
      loadPost
      getPost
    } 

    Thừa nhận đi ! Nhìn dễ hiểu hơn Vuex nhiều phải không ! Đồng thời cũng chả phải học thêm bất kì cái gì để hiểu

    *Composition API là cái bắt buộc biết rồi nên k tính ! haha :))

    Ở dự án Vue Storefront chúng tôi cũng dùng như thế này (nhưng là với reactive thay cho ref)

    import { reactive, computed } from '@vue/composition-api';
    
    const state = reactive({
      isCartSidebarOpen: false,
      isLoginModalOpen: false
    });
    
    const isCartSidebarOpen = computed(() => state.isCartSidebarOpen);
    const toggleCartSidebar = () => {
      state.isCartSidebarOpen = !state.isCartSidebarOpen;
    };
    
    const isLoginModalOpen = computed(() => state.isLoginModalOpen);
    const toggleLoginModal = () => {
      state.isLoginModalOpen = !state.isLoginModalOpen;
    };
    
    const uiState = {
      isCartSidebarOpen,
      isLoginModalOpen,
      toggleCartSidebar,
      toggleLoginModal
    };
    
    export default uiState;

    Keeping your state local

    Với Vuex ! best practicekhông thay đổi data trực tiếp ở state mà phải thông qua mutation nhớ không ?

    Lý do là vì Vuex hỗ trợ tracking những thay đổi của state thông qua mutation. Qua đó mà ta có thể dễ dàng debug. Cho nên nếu ta thay đổi state trực tiếp thì chẳng khác gì “dâng vàng cho trộm” đồng thời chẳng biết thằng trộm nào đã lấy vàng đi mất :)) LOL

    Với Composition API, ta cũng có thể keep state local như vậy

    export default function useProduct() {
      const loading = ref(false)
      const products = ref([])
    
      async function search (params) {
        loading.value = true
        products.value = await fetchProduct(params)
        loading.value = false
      }
      return {
        loading: computed(() => loading.value)
        products: computed(() => products.value)
        search
      }
    }

    Bạn nhận thấy gì không ? Thay vì return products & loading object về trực tiếp thì tôi lại thông qua computed properties ?

    Với cách này ta chắc chắn rằng không ai có thể mutate(thay đổi) những objects đó bên ngoài useProduct

    TIP : bạn có thể làm được y change với readonly property từ Composition API
    const original = reactive({ count: 0 })
    
    const copy = readonly(original)
    
    watchEffect(() => {
      // works for reactivity tracking
      console.log(copy.count)
    })
    
    // mutating original will trigger watchers relying on the copy
    original.count++
    
    // mutating the copy will fail and result in a warning
    copy.count++ // warning!

    Bây giờ thì sử dụng useProduct nào !

    <template>
      <div>
        <span v-if="loading">Loading product</span>
        <span v-else>Loaded {{ product.name }}</span>
      </div>
    </template>
    
    <script>
    import { useProduct } from './useProduct'
    
    export default {
      setup (props, context) {
        const id = context.root.$route.params.id
        const { products, loading, search } = useProduct()
    
        search({ id })
    
        return {
          product: computed(() => products.value[0]),
          loading
        }
      }
    }
    </script>

    Chia ứng dụng thành ~ phần độc lập và để chúng giao tiếp với nhau qua APIs không bị phụ thuộc vào implementation details là cách hiệu quả nhất để code organized(có tổ chức) và maintainable(dễ bảo trì).

    Sharing state between composition functions

    Có một điều như này ! Trước tiên hãy nhìn lại vào useProduct function

    export default function useProduct() {
      const loading = ref(false)
      const products = ref([])
    
     . . .
    
      return {
        loading: computed(() => loading.value)
        products: computed(() => products.value)
        search
      }
    }

    State được đặt nằm trong function. Chính vì điều đó nên mỗi lần ta import & sử dụng function useProduct => state sẽ được tạo mới. Do đó nếu sử dụng ở nhiều Component khác nhau state sẽ chẳng được share với nhau

    Ví dụ ở SomeComponent.vue
    <template>
      <div>
        Items in cart<b>{{ cart.items.length }}</b>
      </div>
    </template>
    
    <script>
    export default {
      setup () {
        const { cart } = useCart()
        return { cart }
      }
    </script>

    Và chúng ta có button addProductToCart ở OtherComponent.vue như này

    <template>
      <div>
        <button @click="addToCart(product)">Add to cart</button>
      </div>
    </template>
    
    <script>
    export default {
      setup () {
        const { products, search } = useProduct()
        const { cart, addToCart } = useCart()
    
        search({ id: '123' })
    
        return { 
          cart, 
          addToCart.
          product: computed(() products.value[0])
         }
      }
    </script>

    Ta muốn cả 2 Components đều refer(hướng đến) cùng 1 object chung là products để khi mà click “Add to cart” button ở OtherComponent thì kết quả cũng sẽ hiện ở SomeComponent ngay lập tức

    Ta có thể đạt được điều này bằng 2 cách :

    1. Sử dụng cart object từ external store
    // store.js
    const state = ref({ cart: {} })
    const setCart = (cart) => { state.value.cart = cart }
    
    export { setCart }
    // useCart.js
    import { setCart, cart } from './store.js'
    
    export function useCart () {
      // use setCart and cart here
    }

    Mặc dù vấn dễ đã được giải quyết nhưng có 2 nhược điểm :

    • useCart method(ở useCart.js) bây giờ lại bị phụ thuộc vào store.js làm cho nó less self-contained and reusable (khó tái sử dụng)
    • thuộc tính cart ở trong state object do nằm độc lập bên ngoài useCart function nên rất dễ bị xót khi ta removing/changing useCart function (ý là nếu xóa đi useCart thì nhiều khi state liên quan vẫn còn nằm đó mà không được sử dụng)

    2. Best Practice

    Trong useCart.js

    const cart = ref({})
    
    function useCart () {
      // super complicated cart logic
      return {
        cart: computed(() => cart.value)
      }
    }

    Voila – Chỉ cần đặt cart ra bên ngoài function là đủ !

    Như vậy mỗi lần ta gọi const { cart, addToCart } = useCart() state sẽ vẫn giữ nguyên dù init bao nhiêu function useCart() đi chăng nữa !

    Tóm lại

    Composition API không chỉ là một cách mới và mang tính cách mạng về chia sẽ state, reuseable code giữa components mà còn lại một sự thay thế tuyệt vời cho Vuex

    API mới này đơn giản hóa code của bạn và thêm vào đó cải thiện kiến trúc hệ thống

    Không thể ngừng chờ xem Composition API kết hợp với Pinia sẽ ảnh hướng ta như thế nào :))) haha

    Published inDành cho mọi ngườiVue Core

    Be First to Comment

    Leave a Reply

    Your email address will not be published. Required fields are marked *