Extensões
As memórias da Pinia podem ser completamente estendidas através duma API de baixo nível. Eis uma lista de coisas que podemos fazer:
- Adicionar novas propriedades às memórias
- Adicionar novas opções quando definimos memórias
- Adicionar novos métodos às memórias
- Embrulhar os métodos existentes
- Intercetar ações e seus resultados
- Implementar efeitos colaterais tais como Armazenamento Local
- Aplicar apenas às memórias especificas
As extensões são adicionas à instância de pinia com pinia.use(). O exemplo mais simples é adicionar uma propriedade estática em todas as memórias retornando um objeto:
import { createPinia } from 'pinia'
// adicionar uma propriedade `secret` em toda
// memória criada depois desta extensão ser
// instalada, isto poderia estar
// num ficheiro diferente
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// atribuir a extensão à pinia
pinia.use(SecretPiniaPlugin)
// num outro ficheiro
const store = useStore()
store.secret // 'the cake is a lie'Isto é útil para adicionar objetos tais como roteador, modal, ou gestores de brinde.
Introdução
Uma extensão de Pinia é uma função que retorna opcionalmente as propriedades a serem adicionadas à uma memória. Esta recebe um argumento opcional, um contexto:
export function myPiniaPlugin(context) {
context.pinia // pinia criada com `createPinia()`
context.app // aplicação atual criada com `createApp()` (só na Vue 3)
context.store // memória manipulada pela extensão
context.options // objeto de opções da memória passada a `defineStore()`
// ...
}Esta função é então passada à pinia com pinia.use():
pinia.use(myPiniaPlugin)As extensão apenas são aplicadas às memórias criadas depois das próprias extensões, e depois da pinia ser passada à aplicação, de outro modo não serão aplicadas.
Aumentando uma Memória
Nós podemos adicionar as propriedades a toda memória simplesmente retornando um objeto destas numa extensão:
pinia.use(() => ({ hello: 'world' }))Nós também podemos definir a propriedade diretamente sobre a store mas se possível usamos a versão de retorno, assim podem ser rastreadas automaticamente pelas ferramentas de programação do navegador:
pinia.use(({ store }) => {
store.hello = 'world'
})Qualquer propriedade retornada por uma extensão será rastreada automaticamente pelas ferramentas de programação do navegador, então no sentido de tornar hello visível nas ferramentas de programação do navegador, devemos certificar-nos de adicioná-la à store._customProperties apenas no modo de desenvolvimento se quisermos depurá-la nas ferramentas de programação do navegador:
// a partir do exemplo acima
pinia.use(({ store }) => {
store.hello = 'world'
// garantir que o empacotador manipule isto.
// a webpack e vite devem fazê-lo por padrão
if (process.env.NODE_ENV === 'development') {
// adicionar quaisquer chaves que definimos na memória
store._customProperties.add('hello')
}
})Nota que toda memória é embrulhada com a reactive, desembrulhando automaticamente qualquer referência (ref(), computed(), ...) que esta contiver:
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// cada memória tem sua propriedade `hello`
store.hello = ref('secret')
// é desembrulhada automaticamente
store.hello // 'secret'
// todas memórias estão partilhando
// o valor da propriedade `shared`
store.shared = sharedRef
store.shared // 'shared'
})É por causa disto que podemos acessar todas as propriedades computadas sem .value e por isto são reativas.
Adicionando Novo Estado
Se quisermos adicionar novas propriedades de estado à uma memória ou às propriedades que estão destinadas a serem usadas durante a hidratação, precisaremos adicioná-lo em dois lugares:
- Na
store, assim podemos acessá-lo comstore.myState - Na
store.$state, assim pode ser usada nas ferramentas de programação do navegador e, ser serializada durante a interpretação do lado do servidor.
Além de que, certamente precisaremos usar uma ref() (ou outra API reativa) para partilhar o valor através de diferentes acessos:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// para manipular corretamente a interpretação do lado servidor,
// precisamos garantir que não estamos sobrepondo
// um valor existente
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
// `hasError` é definida dentro da extensão,
// assim cada memória tem sua propriedade de estado
const hasError = ref(false)
// definir a variável na `$state`, permite que esta seja
// serializada durante a interpretação do lado do servidor
store.$state.hasError = hasError
}
// precisamos transferir a `ref` de `state` para a `store`,
// desta maneira ambos acessos: `store.hasError` e
// `store.$state.hasError` funcionarão e
// partilharão a mesma variável
// Consulte https://pt.vuejs.org/api/reactivity-utilities#toref
store.hasError = toRef(store.$state, 'hasError')
// neste caso é melhor não retornar `hasError` visto que
// será exibida na secção `state` nas ferramentas de programação
// e se a retornarmos, as ferramentas de programação a exibirão
// duas vezes.
})Nota que as mudanças de estado ou adições que ocorrem dentro duma extensão (que inclui chamar store.$patch()) acontecem antes da memória estar ativa e portanto não aciona quaisquer subscrições.
AVISO
Se estivermos usando a Vue 2, a Pinia está sujeita às mesmas advertências de reatividade conforme a Vue. Nós precisaremos usar Vue.set() (Vue 2.7) ou set() (do @vue/composition-api para a Vue <2.7) para quando criarmos as novas propriedades de estado como secret e hasError:
import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!Object.prototype.hasOwnProperty(store.$state, 'secret')) {
const secretRef = ref('secret')
// Se o dado estiver destinado a ser usado durante a
// interpretação do lado do servidor, devemos
// defini-lo na propriedade `$state`, assim é
// serializado e recuperado durante a hidratação
set(store.$state, 'secret', secretRef)
}
// também o definimos diretamente na memória,
// assim podemos acessá-lo de duas maneiras:
// `store.$state.secret` / `store.secret`
set(store, 'secret', toRef(store.$state, 'secret'))
store.secret // 'secret'
})Redefinindo o Estado adicionado nas Extensões
Por padrão, $reset() reiniciará o estado adicionado pelas extensões mas podemos sobrepor este para reiniciar o estado que adicionamos:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// este é o mesmo código de cima por referência
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
// temos de nos certificar de definir o
// contexto (`this`) à memória
const originalReset = store.$reset.bind(store)
// sobrepor a função `$reset`
return {
$reset() {
originalReset()
store.hasError = false
},
}
})Adicionando Novas Propriedades Externas
Quando adicionamos propriedades externas, as instâncias de classe que vêm de outras bibliotecas, ou simplesmente coisas que não são reativas, devemos embrulhar o objeto com markRaw() antes de passá-lo à pinia. Eis um exemplo adicionando o roteador à toda memória:
import { markRaw } from 'vue'
// adaptar isto baseado em onde o nosso roteador está
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})Chamando $subscribe dentro das Extensões
Nós também podemos usar store.$subscribe e store.$onAction dentro das extensões:
pinia.use(({ store }) => {
store.$subscribe(() => {
// reagir às mudanças da memória
})
store.$onAction(() => {
// reagir às ações da memória
})
})Adicionando Novas Opções
É possível criar novas opções quando definimos as memórias para depois as consumir a partir das extensões. Por exemplo, poderíamos criar uma opção debounce que permite-nos reduzir a chamada de qualquer ação:
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// esta depois será lida por uma extensão
debounce: {
// reduzir a chamada da ação `searchContacts` por 300ms
searchContacts: 300,
},
})A extensão depois pode ler esta opção para embrulhar as ações e substituir as originais:
// usar qualquer biblioteca de `debounce`
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// estamos sobrepondo as ações com as novas
return Object.keys(options.debounce).reduce(
(debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})Nota que as opções personalizadas são passadas como terceiro argumento quando escrevemos a sintaxe de configuração (ou setup):
defineStore(
'search',
() => {
// ...
},
{
// esta depois será lido por uma extensão
debounce: {
// reduzir a chamada da ação `searchContacts` por 300ms
searchContacts: 300,
},
}
)TypeScript
Tudo que foi mostrado acima pode ser feito com suporte de tipificação, então nunca mais precisaremos usar any ou @ts-ignore.
Tipificando as Extensões
Uma extensão de Pinia pode ser tipificada da seguinte maneira:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}Tipificando Novas Propriedades da Memória
Quando adicionamos novas propriedades à memória, também devemos estender a interface PiniaCustomProperties:
import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
// usando um definidor podemos permitir ambas
// sequências de caracteres e referências
set hello(value: string | Ref<string>)
get hello(): string
// também podemos definir valores mais simples
simpleNumber: number
// tipificar o roteador adicionado
// pela extensão acima (#adding-new-external-properties)
router: Router
}
}Isto pode então ser escrito e lido com segurança:
pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.simpleNumber = Math.random()
// @ts-expect-error: nós não tipificamos isto corretamente
store.simpleNumber = ref(Math.random())
})PiniaCustomProperties é um tipo genérico que permite-nos referenciar propriedades duma memória. Suponhamos que o seguinte exemplo onde copiamos as opções iniciais como $options (isto apenas funcionaria para memórias de opções):
pinia.use(({ options }) => ({ $options: options }))Nós podemos tipificar isto corretamente usando os 4 tipos genéricos de PiniaCustomProperties:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}DICA
Quando estendemos os tipos em genéricos, estes deve ser nomeados exatamente como estão no código-fonte. Id não pode ser nomeado id ou I, e S não pode ser nomeado State. Eis o que cada letra significa:
- S: State (Estado)
- G: Getters (Recuperadores)
- A: Actions (Ações)
- SS: Setup Store / Store (Memória de Configuração / Memória)
Tipificando Novo Estado
Quando adicionamos novas propriedades de estado (à ambas, a store e store.$state), precisamos adicionar o tipo ao PiniaCustomStateProperties. Diferentemente de PiniaCustomProperties, este apenas recebe o State genérico:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}Tipificando Novas Opções de Criação
Quando criamos novas opções para defineStore(), devemos estender o DefineStoreOptionsBase. Diferentemente de PiniaCustomProperties, apenas expõe dois genéricos: o tipo State e o Store, permitindo-nos limitar o que pode ser definido. Por exemplo, podemos usar os nomes das ações:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// permitir a definição dum número de `ms`
// para quaisquer uma das ações
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}DICA
Também existe um tipo StoreGetters para extrair os recuperadores a partir dum tipo Store. Nós também podemos estender as opções das memórias de configuração ou memórias de opção apenas estendendo os tipos DefineStoreOptions e DefineSetupStoreOptions respetivamente.
Nuxt.js
Quando usamos a pinia em conjunto com a Nuxt, primeiro precisaremos criar uma extensão de Nuxt. Isto dar-nos-á à instância de pinia:
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// reagir às mudanças da memória
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Nota que isto precisa ser tipificado se usamos TypeScript
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})INFORMAÇÃO
O exemplo acima estiver usando a TypeScript, precisamos remover as anotações de tipo PiniaPluginContext e Plugin bem como as suas importações se usarmos um ficheiro .js.
Nuxt.js 2
Se usarmos a Nuxt.js 2, os tipos são ligeiramente diferentes:
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// reagir às mudanças da memória
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Nota que isto precisa ser tipificado se usamos TypeScript
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
}
export default myPlugin