# Introdução

# Porque API de Composição?

Nota

Chegando até esta etapa da documentação, você já deve estar familiarizado com os fundamentos do Vue e o básico sobre componentes.

Assista a um vídeo gratuito sobre a API de Composição no Vue Mastery

A criação de componentes Vue nos permite extrair partes repetíveis da interface, juntamente com sua funcionalidade, em partes reutilizáveis de código. Apenas isso já pode levar nossa aplicação bem longe em termos de manutenibilidade e flexibilidade. No entanto, nossa experiência coletiva provou que isso por si só pode não ser suficiente, especialmente quando a aplicação está ficando muito grande - algo como centenas de componentes. Ao lidar com aplicações tão grandes, compartilhar e reutilizar o código torna-se importantíssimo.

Vamos imaginar que em nossa aplicação temos uma tela (view) para mostrar uma lista de repositórios de um determinado usuário. Além disso, queremos aplicar recursos de pesquisa e de filtros. O componente que manipula essa visualização pode ser assim:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // usando `this.user` para buscar os repositórios de usuário
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

Este componente tem várias responsabilidades:

  1. Obter repositórios para esse nome de usuário de uma API, presumivelmente externa, e atualizá-los sempre que o usuário mudar;
  2. Procurar repositórios usando uma string searchQuery; e
  3. Filtrar repositórios usando um objeto filters.

Organizar lógicas com as opções de componentes (data, computed, methods, watch) funciona na maioria dos casos. No entanto, quando nossos componentes ficam maiores, a lista de preocupações lógicas também aumenta. Isso pode levar a componentes que são difíceis de ler e entender, especialmente para pessoas que não os escreveram.

API de Opções do Vue: Código agrupado por tipo de opção

Exemplo apresentando um grande componente em que suas preocupações lógicas são agrupadas por cores.

Essa fragmentação é o que torna difícil entender e manter um componente complexo. A separação de opções obscurece as preocupações lógicas subjacentes. Além disso, ao trabalhar em uma única preocupação lógica, temos que "pular" constantemente os blocos de opções para o código relevante.

Seria muito mais agradável se pudéssemos colocar o código relacionado à mesma preocupação lógica. E é exatamente isso que a API de Composição nos permite fazer.

# Noções Básicas da API de Composição

Agora que já sabemos o porquê, podemos chegar ao como. Para começar a trabalhar com a API de Composição, primeiro precisamos de um lugar onde possamos realmente usá-la. Em um componente Vue, chamamos esse local de setup.

# Opção de Componente setup

A nova opção de componente setup é executada antes do componente ser criado, uma vez que as props estão resolvidas, e serve como ponto de entrada para APIs de composição.

Aviso

Você deve evitar usar this dentro de setup, pois não fará referência à instância do componente. setup é chamado antes das propriedades data, computed ou methods serem resolvidas, então elas não estarão disponíveis em setup.

A opção setup deve ser uma função que aceita props e context, sobre os quais falaremos depois. Além disso, tudo o que retornamos de setup será exposto ao resto do nosso componente (dados computados, métodos, gatilhos de ciclo de vida e assim por diante), bem como ao template do componente.

Vamos adicionar setup ao nosso componente:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    console.log(props) // { user: '' }

    return {} // qualquer dado retornado aqui estará disponível para o restante do componente
  }
  // o restante do componente
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Agora vamos começar extraindo a primeira preocupação lógica (marcada como "1" no trecho original).

  1. Obter repositórios para esse nome de usuário de uma API, presumivelmente externa, e atualizá-los sempre que o usuário mudar

Começaremos com as partes mais óbvias:

  • A lista de repositórios;
  • A função para atualizar a lista de repositórios; e
  • Retornar a lista e a função para que sejam acessíveis por outras opções de componente.
// função `setup` de src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'

// dentro do nosso componente
setup (props) {
  let repositories = []
  const getUserRepositories = async () => {
    repositories = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories // as funções retornadas se comportam da mesma forma que os métodos
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Este é o nosso ponto de partida, exceto que ainda não está funcionando porque nossa variável repositories não é reativa. Isso significa que, da perspectiva do usuário, a lista de repositórios permaneceria vazia. Vamos consertar isso!

# Variáveis Reativas com ref

No Vue 3.0, podemos tornar qualquer variável reativa em qualquer lugar com uma nova função ref, como:

import { ref } from 'vue'

const counter = ref(0)
1
2
3

ref pega o argumento e o retorna encapsulado em um objeto com uma propriedade value, que pode ser usada para acessar ou alterar o valor da variável reativa:

import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

counter.value++
console.log(counter.value) // 1
1
2
3
4
5
6
7
8
9

Encapsular valores dentro de um objeto pode parecer desnecessário, mas é preciso para manter o comportamento unificado em diferentes tipos de dados em JavaScript. Isso ocorre porque, em JavaScript, tipos primitivos como Number ou String são passados por valor, não por referência:

Passagem por referência vs passagem por valor

Ter um objeto encapsulado com qualquer valor nos permite passá-lo com segurança por toda a nossa aplicação, sem a preocupação de perder sua reatividade em algum lugar ao longo do caminho.

Nota

Em outras palavras, ref cria uma Referência Reativa para nosso valor. O conceito de trabalhar com Referências será usado frequentemente em toda a API de Composição.

De volta ao nosso exemplo, vamos criar uma variável repositories reativa:

// função `setup` de src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

// no nosso componente
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Feito! Agora, sempre que chamarmos getUserRepositories, a variável repositories será modificada e a visualização será atualizada para refletir a mudança. Nosso componente agora deve ter a seguinte aparência:

// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const repositories = ref([])
    const getUserRepositories = async () => {
      repositories.value = await fetchUserRepositories(props.user)
    }

    return {
      repositories,
      getUserRepositories
    }
  },
  data () {
    return {
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Nós movemos várias partes de nossa primeira preocupação lógica para o método setup, bem colocadas próximas umas das outras. O que resta é chamar getUserRepositories no gatilho mounted e configurar um observador para fazer isso sempre que a propriedade user mudar.

Começaremos com o gatilho de ciclo de vida.

# Registro de Gatilho de Ciclo de Vida no setup

Para tornar a API de Composição completa em comparação com a API de Opções, também precisamos de uma maneira de registrar gatilhos de ciclo de vida dentro de setup. Isso é possível graças a várias novas funções exportadas do Vue. Gatilhos de ciclo de vida na API de Composição têm o mesmo nome da API de Opções, mas são prefixados com on: ou seja, mounted seria parecido com onMounted.

Essas funções aceitam um retorno de chamada (callback) que será executado quando o gatilho for chamado pelo componente.

Vamos adicioná-lo à nossa função setup:

// função `setup` de src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'

// no nosso componente
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  onMounted(getUserRepositories) // quando `mounted`, chamar `getUserRepositories`

  return {
    repositories,
    getUserRepositories
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Agora precisamos reagir às mudanças feitas na propriedade user. Para isso, usaremos a função independente watch.

# Reagindo às Mudanças com watch

Assim como configuramos um observador na propriedade user dentro de nosso componente usando a opção watch, podemos fazer o mesmo usando a função watch importada do Vue. Três argumentos são aceitos:

  • Uma Referência Reativa ou função getter que queremos observar;
  • Um retorno de chamada (callback); e
  • Opções de configuração opcionais.

Aqui está um exemplo de como funciona:

import { ref, watch } from 'vue'

const counter = ref(0)
watch(counter, (newValue, oldValue) => {
  console.log('O novo valor do contador é: ' + counter.value)
})
1
2
3
4
5
6

Sempre que counter é modificado, por exemplo counter.value = 5, o watch irá disparar e executar o retorno de chamada (segundo argumento) que neste caso irá registrar 'O novo valor do contador é: 5' em nosso console.

Abaixo está o equivalente na API de Opções:

export default {
  data() {
    return {
      counter: 0
    }
  },
  watch: {
    counter(newValue, oldValue) {
      console.log('O novo valor do contador é: ' + this.counter)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

Para mais detalhes sobre watch, consulte nosso guia aprofundado.

Vamos agora aplicá-lo ao nosso exemplo:

// função `setup` de src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'

// in our component
setup (props) {
  // usando `toRefs` para criar uma Referência Reativa à propriedade `user` de `props`
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // atualiza `props.user` para `user.value`, para acessar o valor da referência
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // define um observador na Referência Reativa para a propriedade `user`
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Você provavelmente notou o uso de toRefs no topo de nosso setup. Isso é para garantir que nosso observador reaja às mudanças feitas na propriedade user.

Com essas mudanças em vigor, acabamos de mover toda a primeira preocupação lógica para um único lugar. Agora podemos fazer o mesmo com a segunda preocupação - filtrar com base em searchQuery, desta vez com um dado computado.

# Propriedades computed Independentes

Semelhante a ref e watch, as dados computados também podem ser criados fora de um componente Vue com a função computed importada do Vue. Vamos voltar ao nosso contra-exemplo:

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
1
2
3
4
5
6
7
8

Aqui, a função computed retorna uma Referência Reativa de somente leitura na saída do retorno da chamada "estilo getter", passado como primeiro argumento para computed. Para acessar o atributo value da variável computada recém-criada, precisamos usar a propriedade .value assim como com ref.

Vamos mover nossa funcionalidade de pesquisa para setup:

// função `setup` de src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

// no nosso componente
setup (props) {
  // usando `toRefs` para criar uma Referência Reativa para a propriedade `user`
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // atualiza `props.user` para `user.value`, para acessar o valor da referência
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // define um observador na Referência Reativa para a propriedade `user`
  watch(user, getUserRepositories)

  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(
      repository => repository.name.includes(searchQuery.value)
    )
  })

  return {
    repositories,
    getUserRepositories,
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

Poderíamos fazer o mesmo com outras preocupações lógicas, mas você já deve estar se perguntando – Isso não é apenas mover o código para a opção setup e torná-lo extremamente grande? Bem, é verdade. É por isso que antes de prosseguirmos com as outras responsabilidades, primeiro iremos extrair o código acima em uma função de composição autônoma. Vamos começar criando useUserRepositories:

// src/composables/useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

E então a funcionalidade de pesquisa:

// src/composables/useRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Agora que temos essas duas funcionalidades em arquivos separados, podemos começar a usá-las em nosso componente. Veja como isso pode ser feito:

// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    return {
      // Uma vez que realmente não nos importamos com os repositórios não filtrados
      // podemos expor os resultados filtrados sob o nome de `repositories`
      repositories: repositoriesMatchingSearchQuery,
      getUserRepositories,
      searchQuery,
    }
  },
  data () {
    return {
      filters: { ... }, // 3
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
  },
  methods: {
    updateFilters () { ... }, // 3
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Neste ponto, você provavelmente já conhece o procedimento, então vamos pular para o final e migrar a funcionalidade de filtragem restante. Na verdade, não precisamos entrar em detalhes de implementação, pois esse não é o objetivo deste guia.

// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      // Uma vez que realmente não nos importamos com os repositórios não filtrados
      // podemos expor os resultados finais sob o nome de `repositories`
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

E nós terminamos!

Tenha em mente que apenas arranhamos a superfície da API de Composição e o que ela nos permite fazer. Para saber mais sobre isso, consulte o guia detalhado.