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
.valueno 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>