Trong thế giới phát triển ứng dụng web với Vue.js, trạng thái (state) đóng vai trò quan trọng và thường là trung tâm của mọi (store). Ở Cafedev, chúng tôi đánh giá cao sự quan trọng của việc hiểu rõ về cách làm việc với trạng thái trong Pinia Vue.js. Trong bài viết này về Pinia with State, chúng ta sẽ khám phá sâu hơn về cách quản lý và tương tác với trạng thái trong ứng dụng Vue.js sử dụng thư viện Pinia.

Trạng thái (state) thường là phần trung tâm của cửa hàng (store) của bạn. Mọi người thường bắt đầu bằng cách định nghĩa trạng thái đại diện cho ứng dụng của họ. Trong Pinia, trạng thái được định nghĩa như một hàm trả về trạng thái ban đầu. Điều này cho phép Pinia hoạt động cả trên Server và Client Side.

import { defineStore } from 'pinia'

export const useStore = defineStore('storeId', {
  // arrow function recommended for full type inference
  state: () => {
    return {
      // all these properties will have their type inferred automatically
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})

Mẹo: Nếu bạn đang sử dụng Vue 2, dữ liệu bạn tạo trong state tuân theo cùng các quy tắc như data trong một thể hiện Vue, tức là đối tượng trạng thái phải là đơn giản và bạn cần gọi Vue.set() khi thêm các thuộc tính mới vào đó. Xem thêm: Vue#data.

1. TypeScript

Bạn không cần phải làm nhiều để làm cho trạng thái của bạn tương thích với TS: đảm bảo strict, hoặc ít nhất là, noImplicitThis, được kích hoạt và Pinia sẽ suy luận loại của trạng thái của bạn tự động! Tuy nhiên, có một số trường hợp bạn nên giúp nó với một số ép kiểu:

export const useUserStore = defineStore('user', {
  state: () => {
    return {
      // for initially empty lists
      userList: [] as UserInfo[],
      // for data that is not yet loaded
      user: null as UserInfo | null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

Nếu bạn muốn, bạn có thể xác định trạng thái bằng một giao diện và gõ kiểu cho giá trị trả về của state():

interface State {
  userList: UserInfo[]
  user: UserInfo | null
}

export const useUserStore = defineStore('user', {
  state: (): State => {
    return {
      userList: [],
      user: null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

2. Truy cập vào state

Theo mặc định, bạn có thể đọc và ghi trực tiếp vào trạng thái bằng cách truy cập qua thể hiện store:

const store = useStore()

store.count++

Lưu ý rằng bạn không thể thêm một thuộc tính trạng thái mới nếu bạn không xác định nó trong state(). Nó phải chứa trạng thái ban đầu. Ví dụ: chúng ta không thể làm store.secondCount = 2 nếu secondCount không được xác định trong state().

3. Đặt lại trạng thái

Trong Option Stores, bạn có thể đặt lại trạng thái về giá trị ban đầu bằng cách gọi phương thức $reset() trên cửa hàng:

const store = useStore()

store.$reset()

Nội bộ, điều này gọi hàm state() để tạo một đối tượng trạng thái mới và thay thế trạng thái hiện tại bằng nó.
Trong Setup Stores, bạn cần tạo ra phương thức $reset() của riêng bạn:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

3.1 Sử dụng với API Option


Đối với các ví dụ sau, bạn có thể giả định cửa hàng sau đã được tạo:

// Example File Path:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
})

Nếu bạn không sử dụng Composition API, và bạn đang sử dụng computed, methods, …, bạn có thể sử dụng trợ giúp mapState() để ánh xạ các thuộc tính trạng thái như các thuộc tính computed chỉ đọc:

import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // gives access to this.count inside the component
    // same as reading from store.count
    ...mapState(useCounterStore, ['count'])
    // same as above but registers it as this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'count',
      // you can also write a function that gets access to the store
      double: store => store.count * 2,
      // it can have access to `this` but it won't be typed correctly...
      magicValue(store) {
        return store.someGetter + this.count + this.double
      },
    }),
  },
}

Trạng thái có thể sửa đổi

Nếu bạn muốn có khả năng ghi vào các thuộc tính trạng thái này (ví dụ: nếu bạn có một biểu mẫu), bạn có thể sử dụng mapWritableState() thay vì. Lưu ý bạn không thể truyền một hàm giống như với mapState():

import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // gives access to this.count inside the component and allows setting it
    // this.count++
    // same as reading from store.count
    ...mapWritableState(useCounterStore, ['count']),
    // same as above but registers it as this.myOwnName
    ...mapWritableState(useCounterStore, {
      myOwnName: 'count',
    }),
  },
}

tip Bạn không cần mapWritableState() cho các bộ sưu tập như mảng trừ khi bạn thay thế toàn bộ mảng với cartItems = [], mapState() vẫn cho phép bạn gọi phương thức trên các bộ sưu tập của mình.

4. Thay đổi trạng thái

Ngoài việc thay đổi trực tiếp cửa hàng với store.count++, bạn cũng có thể gọi phương thức $patch. Nó cho phép bạn áp dụng nhiều thay đổi cùng một lúc với một đối tượng state phần một:

store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

Tuy nhiên, một số sự biến đổi thực sự khó hoặc tốn kém để áp dụng với cú pháp này: bất kỳ sự biến đổi bộ sưu tập nào (ví dụ: đẩy, loại bỏ, cắt một phần tử từ một mảng) đều đòi hỏi bạn phải tạo ra một bộ sưu tập mới. Vì vậy, phương thức $patch cũng chấp nhận một hàm để nhóm những loại biến đổi này mà khó áp dụng với một đối tượng patch:

store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

Sự khác biệt chính ở đây là $patch() cho phép bạn nhóm nhiều thay đổi thành một mục duy nhất trong công cụ devtools. Lưu ý rằng cả các thay đổi trực tiếp vào state$patch() đều xuất hiện trong devtools và có thể duyệt thời gian (chưa có trong Vue 3).

5. Thay thế state

Bạn không thể thay thế chính xác trạng thái của một cửa hàng vì điều đó sẽ phá vỡ tính phản ứng. Tuy nhiên, bạn có thể khoá patch:

// this doesn't actually replace `$state`
store.$state = { count: 24 }
// it internally calls `$patch()`:
store.$patch({ count: 24 })

Bạn cũng có thể đặt lại trạng thái ban đầu của toàn bộ ứng dụng của bạn bằng cách thay đổi state của thể hiện pinia. Điều này được sử dụng trong quá trình SSR for hydration.

pinia.state.value = {}

6. Đăng ký vào trạng thái

Bạn có thể theo dõi trạng thái và các thay đổi của nó thông qua phương thức $subscribe() của một cửa hàng, tương tự như phương thức subscribe của Vuex. Ưu điểm của việc sử dụng $subscribe() hơn so với watch() thông thường là các đăng ký chỉ kích hoạt một lần sau mỗi patch (ví dụ: khi sử dụng phiên bản hàm từ phía trên).

cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // same as cartStore.$id
  mutation.storeId // 'cart'
  // only available with mutation.type === 'patch object'
  mutation.payload // patch object passed to cartStore.$patch()

  // persist the whole state to the local storage whenever it changes
  localStorage.setItem('cart', JSON.stringify(state))
})

Theo mặc định, các đăng ký trạng thái được ràng buộc với thành phần mà chúng được thêm vào (nếu cửa hàng nằm trong setup() của một thành phần). Có nghĩa là chúng sẽ tự động được loại bỏ khi thành phần bị hủy lắp. Nếu bạn muốn giữ chúng sau khi thành phần bị hủy lắp, bạn cũng có thể truyền { detached: true } như là đối số thứ hai để tách rời đăng ký trạng thái khỏi thành phần hiện tại:

<script setup>
const someStore = useSomeStore()

// this subscription will be kept even after the component is unmounted
someStore.$subscribe(callback, { detached: true })
</script>

tip Bạn có thể theo dõi toàn bộ trạng thái trên thể hiện pinia với một watch() duy nhất:

watch(
  pinia.state,
  (state) => {
    // persist the whole state to the local storage whenever it changes
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Trong bài viết Router Vuejs with State trên Cafedev, chúng ta đã khám phá cách quản lý trạng thái trong Vue.js sử dụng Pinia. Từ việc định nghĩa trạng thái đến cách truy cập và thay đổi nó, chúng ta đã có cái nhìn tổng quan về cách làm việc với trạng thái một cách hiệu quả. Hy vọng rằng thông tin này sẽ giúp bạn xây dựng các ứng dụng Vue.js mạnh mẽ và linh hoạt hơn trong tương lai.

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!