API de Composição do Vue 3 e Pinia para Gerenciamento de Estado

Introdução à API de Composição

A API de Composição do Vue 3 permite uma organização mais modular da lógica dos componentes, agrupando funcionalidades relacionadas. Este artigo aborda seus recursos principais e a integração com o Pinia para gerenciamento de estado.

Diferenças entre reactive e ref

Ambos os métodos criam dados reativos, mas com diferentes aplicações:

  • reactive: Ideal para objetos ou arrays, mantendo a reatividade em propriedades aninhadas.
  • ref: Suporta tipos primitivos e objetos, acessado via propriedade .value no script.

Exemplo com contador usando reactive:

<script setup>
import { reactive } from 'vue'

const estado = reactive({
  total: 0
})

const incrementarTotal = () => {
  estado.total++
}
</script>

<template>
  <button @click="incrementarTotal">{{ estado.total }}</button>
</template>

Exemplo equivalente com ref:

<script setup>
import { ref } from 'vue'

const total = ref(0)

const incrementarTotal = () => {
  total.value++
}
</script>

<template>
  <button @click="incrementarTotal">{{ total }}</button>
</template>

Propriedades Computadas

A função computed calcula valores derivados de dados reativos, atualizando automaticamente quando as dependências mudam.

<script setup>
import { ref, computed } from 'vue'

const valores = ref([5, 10, 15, 20, 25])

const mediaValores = computed(() => {
  const soma = valores.value.reduce((acc, val) => acc + val, 0)
  return soma / valores.value.length
})
</script>

<template>
  <div>Valores originais: {{ valores }}</div>
  <div>Média calculada: {{ mediaValores }}</div>
</template>

Função Watch

watch observa mudanças em dados reativos e executa uma função de callback. Suporta opções como immediate (execução inicial) e deep (observação profunda de objetos).

Observando um único dado:

<script setup>
import { ref, watch } from 'vue'

const contador = ref(0)

const incrementar = () => {
  contador.value++
}

watch(contador, (novoValor, valorAnterior) => {
  console.log(`Contador mudou de ${valorAnterior} para ${novoValor}`)
})
</script>

<template>
  <button @click="incrementar">{{ contador }}</button>
</template>

Observando múltiplos dados:

<script setup>
import { ref, watch } from 'vue'

const nome = ref('Alice')
const idade = ref(30)

const atualizarDados = () => {
  nome.value = 'Bob'
  idade.value += 1
}

watch([nome, idade], ([novoNome, novaIdade], [nomeAntigo, idadeAntiga]) => {
  console.log(`Nome: ${nomeAntigo} -> ${novoNome}, Idade: ${idadeAntiga} -> ${novaIdade}`)
})
</script>

<template>
  <button @click="atualizarDados">Atualizar</button>
</template>

Usando immediate para executar na inicialização:

<script setup>
import { ref, watch } from 'vue'

const temperatura = ref(20)

watch(temperatura, (novaTemp, tempAntiga) => {
  console.log(`Temperatura: ${tempAntiga} -> ${novaTemp}`)
}, { immediate: true })
</script>

Observação profunda de objetos com deep:

<script setup>
import { ref, watch } from 'vue'

const perfil = ref({ nome: 'Carlos', local: 'São Paulo' })

const alterarPerfil = () => {
  perfil.value.nome = 'Ana'
}

watch(perfil, (novoPerfil, perfilAntigo) => {
  console.log('Perfil atualizado:', novoPerfil)
}, { deep: true })
</script>

<template>
  <button @click="alterarPerfil">Alterar Perfil</button>
</template>

Ciclo de Vida na API de Composição

Na API de Composição, os ganchos de ciclo de vida são importados como funções. Por exemplo, onMounted executa após a montagem do componente.

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log('Componente montado com sucesso')
})
</script>

Comunicação entre Componentes Pai e Filho

Pai para Filho com Props

Componente pai passa dados via atributos, e o filho declara defineProps para recebê-los.

Componente Pai:

<script setup>
import ComponenteFilho from './ComponenteFilho.vue'
import { ref } from 'vue'

const mensagem = ref('Olá do pai')
</script>

<template>
  <ComponenteFilho :mensagem="mensagem" />
</template>

Componente Filho:

<script setup>
const props = defineProps({
  mensagem: String
})
</script>

<template>
  <div>{{ mensagem }}</div>
</template>

Filho para Pai com Eventos

O pai vincula um evento, e o filho emite usando defineEmits.

Componente Pai:

<script setup>
import ComponenteFilho from './ComponenteFilho.vue'

const tratarMensagem = (texto) => {
  console.log(texto)
}
</script>

<template>
  <ComponenteFilho @enviar-mensagem="tratarMensagem" />
</template>

Componente Filho:

<script setup>
const emit = defineEmits(['enviar-mensagem'])

const enviar = () => {
  emit('enviar-mensagem', 'Mensagem do filho')
}
</script>

<template>
  <button @click="enviar">Enviar</button>
</template>

Referências de Template

Use ref para acessar diretamente elementos do DOM ou instâncias de componentes após a montagem.

<script setup>
import { onMounted, ref } from 'vue'

const elementoRef = ref(null)

onMounted(() => {
  console.log('Elemento DOM:', elementoRef.value)
})
</script>

<template>
  <div ref="elementoRef">Conteúdo referenciado</div>
</template>

Provide e Inject para Comunicação entre Componentes

provide e inject permitem compartilhar dados e métodos entre componentes em diferentes níveis da árvore.

Componente de nível superior (fornecedor):

<script setup>
import { provide, ref } from 'vue'

const dadosCompartilhados = ref('Valor compartilhado')
provide('chave-dados', dadosCompartilhados)

const atualizarDados = () => {
  dadosCompartilhados.value = 'Novo valor'
}
provide('funcao-atualizar', atualizarDados)
</script>

Componente de nível inferior (consumidor):

<script setup>
import { inject } from 'vue'

const dados = inject('chave-dados')
const atualizar = inject('funcao-atualizar')
</script>

<template>
  <div>{{ dados }}</div>
  <button @click="atualizar">Atualizar Dados</button>
</template>

Pinia para Gerenciamento de Estado

Pinia é uma biblioteca de gerenciamento de estado para Vue, substituta do Vuex, com suporte a Composition API.

Configuração Inicial

Instale o Pinia no projeto Vue:

npm install pinia

Configure no arquivo principal:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Criando uma Store

Defina uma store com dados reativos e ações.

// stores/contador.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useContadorStore = defineStore('contador', () => {
  const valor = ref(0)
  const incrementar = () => {
    valor.value++
  }
  return { valor, incrementar }
})

Usando a Store em Componentes

<script setup>
import { useContadorStore } from './stores/contador'

const store = useContadorStore()
</script>

<template>
  <button @click="store.incrementar">{{ store.valor }}</button>
</template>

Getters com computed

Implemente getters usando computed dentro da store.

// stores/contador.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useContadorStore = defineStore('contador', () => {
  const valor = ref(0)
  const incrementar = () => {
    valor.value++
  }
  const valorDobrado = computed(() => valor.value * 2)
  return { valor, valorDobrado, incrementar }
})

Ações Assíncronas

Realize operações assíncronas, como chamadas de API, dentro das ações.

// stores/dados.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'

export const useDadosStore = defineStore('dados', () => {
  const itens = ref([])
  const carregarItens = async () => {
    const resposta = await axios.get('https://api.exemplo.com/itens')
    itens.value = resposta.data
  }
  return { itens, carregarItens }
})

Desestruturação com storeToRefs

Use storeToRefs para manter a reatividade ao dseestruturar a store.

<script setup>
import { useContadorStore } from './stores/contador'
import { storeToRefs } from 'pinia'

const store = useContadorStore()
const { valor, valorDobrado } = storeToRefs(store)
</script>

<template>
  <button @click="store.incrementar">{{ valor }}</button>
  <span>Dobro: {{ valorDobrado }}</span>
</template>

Tags: vue3 composition-api pinia reactive ref

Publicado em 6-1 11:26 por Thomas