Skip to content

State Management: Pinia em hooks/

Esta doc complementa code-style.md. Em caso de conflito, code-style.md vence.

Onde stores moram

Todos os stores Pinia ficam em hooks/useXxx.ts dentro do módulo. Não existem as pastas stores/ nem composables/ por módulo, elas foram unificadas em hooks/.

src/modules/<mod>/
├── services/
├── hooks/        ← stores Pinia + composables, juntos
├── components/
├── pages/
└── router/

Stores globais (sem dono de módulo) ficam em src/hooks/.

Nome do hook: sem sufixo Store

typescript
export const useContracts = defineStore('contracts', () => { ... })
export const useCompliance = defineStore('compliance', () => { ... })

Proibido useContractsStore, useComplianceStore. O id interno do defineStore é livre; o export é sempre useXxx.

Estrutura de uma store

typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

import { getContractsService } from '@/modules/contracts/services/contracts'

import type { Contract, ContractFilter } from '@/domain/contract'

export const useContracts = defineStore('contracts', () => {
  const items = ref<Contract[]>([])
  const total = ref(0)
  const loading = ref(false)
  const search = ref('')

  const activeItems = computed(() => items.value.filter((i) => i.status === 'active'))

  const loadItems = (params?: ContractFilter) => {
    loading.value = true

    return getContractsService(params)
      .then((response) => {
        items.value = response.data.data
        total.value = response.data.total
      })
      .finally(() => {
        loading.value = false
      })
  }

  const setSearch = (value: string) => {
    search.value = value

    return loadItems({ search: value })
  }

  return { items, total, loading, search, activeItems, loadItems, setSearch }
})

Sem try/catch: erros sobem naturalmente; .catch() no consumer mostra e?.response?.data?.message. Use .finally() para liberar loading.

Consumo: storeToRefs + destructuring

typescript
import { storeToRefs } from 'pinia'

import { useContracts } from '@/modules/contracts/hooks/useContracts'

const contracts = useContracts()

const { items, loading, activeItems } = storeToRefs(contracts)

const { loadItems, setSearch } = contracts

loadItems()

contracts (a variável intermediária) só existe para destruturar. Nunca referencie contracts.x depois disso.

Proibido:

  • const store = useContracts() seguido de store.items em template/script, perde reatividade e quebra a regra do split.
  • Wrappar a store em reactive({ ... }): causa dupla indireção e quebra storeToRefs.

Hook complexo: inverter dependências

Quando o defineStore passa de ~100 linhas ou cobre múltiplas responsabilidades, separe em hooks menores em vez de esconder a complexidade em helpers privados. Hooks menores podem importar uns aos outros (sem ciclos), e o componente consome todos.

Padrão de referência: src/modules/contracts/components/Create/Flow/Review/ContactReview.vue.

Tipos não vivem no arquivo do hook

Nada de type Foo = ... em useXxx.ts. Tipos vão para src/domain/<mod>/types.ts (entidades) ou src/modules/<mod>/models/ (UI). Helpers vão para src/modules/<mod>/helpers/<topic>.ts (kebab-case) ou src/domain/<mod>/.

Padrão para listas paginadas

Reusar usePaginatedList em src/composables/usePaginatedList.ts: não duplicar paginação em cada store.