Estado
O estado é, na maioria das vezes, a parte central da nossa memória. As pessoas frequentemente começam definindo o estado que representa a sua aplicação. Na Pinia o estado é definido como uma função que retorna o estado inicial. Isto permite a Pinia funcionar tanto no lado do servidor quanto no lado do cliente:
import { defineStore } from 'pinia' export const useStore = defineStore('storeId', { // função de flecha recomendada para // completa inferência de tipo state: () => { return { // todas estas propriedades terão // seus tipos inferidos automaticamente count: 0, name: 'Eduardo', isAdmin: true, items: [], hasChanged: true, } }, })
DICA
Se estivermos usando a Vue 2, os dados que criamos na state
seguem as mesmas regras aplicadas à data
numa instância de Vue, isto é, o objeto de estado deve ser simples e precisamos chamar Vue.set()
quando adicionamos novas propriedades à este. Consultar também: Vue#data.
TypeScript
Nós precisamos fazer muito no sentido de tornar o nosso estado compatível com a TypeScript: temos que nos certificar de que a strict
, ou no mínimo, a noImplicitThis
, estão ativadas e a Pinia inferirá o tipo do nosso estado automaticamente! No entanto, existem alguns casos onde deveríamos dar-lhe uma mãozinha com alguma moldagem:
export const useUserStore = defineStore('storeId', { state: () => { return { // para listas inicialmente vazias userList: [] as UserInfo[], // para dados que ainda não foram carregados user: null as UserInfo | null, } }, }) interface UserInfo { name: string age: number }
Se preferirmos, podemos definir o estado com uma interface e tipificar o valor do retorno da 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 }
Acessando o state
Por padrão, podemos ler e escrever diretamente ao estado acessando-o através da instância de store
:
const store = useStore() store.counter++
Nota que não podemos adicionar um nova propriedade de estado se não a definimos na state()
, esta deve conter o estado inicial, por exemplo: não podemos fazer store.secondCount = 2
se secondCount
não estiver definida na state()
.
Redefinindo o Estado
Nas Memórias de Opções, podemos reiniciar o estado ao seu valor inicial chamando o método $reset()
na store
:
const store = useStore() store.$reset()
Internamente, este chama a função state()
para criar um novo objeto de estado e substitui o estado atual por ele.
Nas Memórias de Composições, precisamos criar o nosso próprio método $reset()
:
export const useCounterStore = defineStore('counter', () => { const count = ref(0) function $reset() { count.value = 0 } return { count, $reset } })
Uso com a API de Opções
Para os seguintes exemplos, podemos assumir que a seguinte memória foi criada:
// Caminho do Ficheiro de Exemplo: // ./src/stores/counterStore.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), })
Se não estivermos usando a API de Composição, e estivermos usando computed
, methods
, ..., podemos usar a auxiliar mapState()
para mapear as propriedades do estado como propriedades computadas de apenas leitura:
import { mapState } from 'pinia' import { useCounterStore } from '../stores/counter' export default { computed: { // dá acesso ao `this.count` dentro do componente // mesmo que ler de `store.count` ...mapState(useCounterStore, ['count']) // mesmo que o de acima exceto que // a regista como `this.myOwnName` ...mapState(useCounterStore, { myOwnName: 'count', // também podemos escrever uma função // que recebe a acesso à memória double: store => store.count * 2, // pode ter acesso ao `this` mas // não será tipificada corretamente... magicValue(store) { return store.someGetter + this.count + this.double }, }), }, }
Estado Modificável
Se quisermos ser capazes de escrever às estas propriedades de estado (por exemplo, se tivermos um formulário). podemos usar a mapWritableState()
. Nota que não podemos passar uma função da mesma maneira que a mapState()
:
import { mapWritableState } from 'pinia' import { useCounterStore } from '../stores/counter' export default { computed: { // dá acesso ao `this.count` dentro do componente // e permite defini-lo `this.count+++` // o mesmo que ler de `store.count` ...mapWritableState(useCounterStore, ['count']) // o mesmo que o de cima exceto que // a regista como `this.myOwnName` ...mapWritableState(useCounterStore, { myOwnName: 'count', }), }, }
DICA
Nós não precisamos da mapWritableState()
para coleções como vetores a menos que estejamos substituindo o vetor inteiro com cartItems = []
, a mapState()
ainda permite-nos chamar os métodos sobre as nossas coleções.
Alterando o Estado
Para além de alterarmos diretamente a memória com store.count++
, também podemos chamar o método $patch
. Este permite-nos aplicar várias mudanças ao mesmo tempo com um objeto state
parcial:
store.$patch({ count: store.count + 1, age: 120, name: 'DIO', })
No entanto, algumas mutações são muito difíceis ou dispendiosas de aplicar-se com esta sintaxe: qualquer modificação da coleção (por exemplo, empurrar, remover, juntar um elemento dum vetor) exige que criemos uma nova coleção. Por causa disto, o método $patch
também aceita uma função para agrupar este tipo de mutações que são difíceis de aplicar com um objeto de remendo:
store.$patch((state) => { state.items.push({ name: 'shoes', quantity: 1 }) state.hasChanged = true })
A principal diferença aqui é que $patch()
permite-nos agrupar várias mudanças dentro duma única entrada na ferramenta de programação do navegador. Nota que ambas, mudanças diretas ao state
e patch
aparecem na ferramenta de programação do navegador e podem ser viajadas no tempo (ainda não na Vue 3).
Substituindo o state
Nós não podemos substituir exatamente o estado duma memória porque isto quebraria a reatividade. No entanto, podemos remendá-lo:
// esta de fato não substitui o `$state` store.$state = { count: 24 } // esta chama internamente o `$patch()`: store.$patch({ count: 24 })
Nós também podemos definir o estado inicial da nossa aplicação inteira mudando a state
da instância de pinia
. Isto é usado durante a Interpretação do Lado do Servidor para hidratação:
pinia.state.value = {}
Subscrevendo ao Estado
Nós podemos observar o estado e suas mudanças através do método $subscribe()
duma memória, semelhante ao método subscribe
da Vuex. A vantagem de usar $subscribe()
em vez da watch()
normal é que as subscrições acionarão apenas uma vez após os remendos (por exemplo, quando usamos a versão de função acima):
cartStore.$subscribe((mutation, state) => { // import { MutationType } from 'pinia' mutation.type // 'direct' | 'patch object' | 'patch function' // o mesmo que `cartStore.$id` mutation.storeId // 'cart' // disponível apenas com `mutation.type` === 'patch object' mutation.payload // objeto de remendo passado para `cartStore.$patch()` // persistir o estado inteiro no armazenamento local sempre que mudar localStorage.setItem('cart', JSON.stringify(state)) })
Por padrão, as subscrições de estado estão vinculadas ao componente onde são adicionadas (se a memória estiver dentro duma setup()
do componente). Querendo dizer que, serão removidas automaticamente quando o componente for desmontado. Se também quisermos preservá-las depois do componente ser desmontado, passamos { detached: true }
como segundo argumento para separar a subscrição de estado do componente atual:
<script setup> const someStore = useSomeStore() // esta subscrição será preservada // mesmo depois do componente ser desmontado someStore.$subscribe(callback, { detached: true }) </script>
DICA
Nós podemos observar o estado inteiro sobre a instância de pinia
com uma única watch()
:
watch( pinia.state, (state) => { // persistir todo o estado no armazenamento local // sempre que for alterado localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } )