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 }
)