Introduction

    Reaching this far in the documentation, you should already be familiar with both the basics of Vue and .

    Creating Vue components allows us to extract repeatable parts of the interface coupled with its functionality into reusable pieces of code. This alone can get our application pretty far in terms of maintainability and flexibility. However, our collective experience has proved that this alone might not be enough, especially when your application is getting really big – think several hundreds of components. When dealing with such large applications, sharing and reusing code becomes especially important.

    Let’s imagine that in our app, we have a view to show a list of repositories of a certain user. On top of that, we want to apply search and filter capabilities. Our component handling this view could look like this:

    This component has several responsibilities:

    1. Getting repositories from a presumedly external API for that user name and refreshing it whenever the user changes
    2. Searching for repositories using a string
    3. Filtering repositories using a filters object

    Organizing logics with component’s options (data, computed, methods, watch) works in most cases. However, when our components get bigger, the list of logical concerns also grows. This can lead to components that are hard to read and understand, especially for people who didn’t write them in the first place.

    Example presenting a large component where its logical concerns are grouped by colors.

    Such fragmentation is what makes it difficult to understand and maintain a complex component. The separation of options obscures the underlying logical concerns. In addition, when working on a single logical concern, we have to constantly “jump” around option blocks for the relevant code.

    It would be much nicer if we could collocate code related to the same logical concern. And this is exactly what the Composition API enables us to do.

    Basics of Composition API

    Now that we know the why we can get to the how. To start working with the Compsition API we first need a place where we can actually use it. In a Vue component, we call this place the setup.

    The new setup component option is executed before the component is created, once the props are resolved, and serves as the entry point for composition API’s.

    WARNING

    Because the component instance is not yet created when setup is executed, there is no this inside a setup option. This means, with the exception of props, you won’t be able to access any properties declared in the component – local state, computed properties or methods.

    The setup option should be a function that accepts props and context which we will talk about . Additionally, everything that we return from setup will be exposed to the rest of our component (computed properties, methods, lifecycle hooks and so on) as well as to the component’s template.

    Let’s add setup to our component:

    1. // src/components/UserRepositories.vue
    2. export default {
    3. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
    4. props: {
    5. user: { type: String }
    6. },
    7. setup(props) {
    8. console.log(props) // { user: '' }
    9. return {} // anything returned here will be available for the rest of the component
    10. }
    11. // the "rest" of the component
    12. }

    Now let’s start with extracting the first logical concern (marked as “1” in the original snippet).

    • The list of repositories
    • The function to update the list of repositories
    • Returning both the list and the function so they are accessible by other component options
    1. // src/components/UserRepositories.vue `setup` function
    2. import { fetchUserRepositories } from '@/api/repositories'
    3. // inside our component
    4. setup (props) {
    5. let repositories = []
    6. const getUserRepositories = async () => {
    7. repositories = await fetchUserRepositories(props.user)
    8. }
    9. return {
    10. repositories,
    11. getUserRepositories // functions returned behave the same as methods
    12. }
    13. }

    This is our starting point, except it’s not working yet because our repositories variable is not reactive. This means from a user’s perspective, the repository list would remain empty. Let’s fix that!

    In Vue 3.0 we can make any variable reactive anywhere with a new ref function, like this:

    1. import { ref } from 'vue'
    2. const counter = ref(0)

    ref takes the argument and returns it wrapped within an object with a value property, which can then be used to access or mutate the value of the reactive variable:

    1. import { ref } from 'vue'
    2. const counter = ref(0)
    3. console.log(counter) // { value: 0 }
    4. console.log(counter.value) // 0
    5. counter.value++
    6. console.log(counter.value) // 1

    Wrapping values inside an object might seem unnecessary but is required to keep the behavior unified across different data types in JavaScript. That’s because in JavaScript, primitive types like Number or String are passed by value, not by reference:

    Pass by reference vs pass by value

    Having a wrapper object around any value allows us to safely pass it across our whole app without worrying about losing its reactivity somewhere along the way.

    Note

    In other words, ref creates a Reactive Reference to our value. The concept of working with References will be used often throughout the Composition API.

    Back to our example, let’s create a reactive repositories variable:

    Done! Now whenever we call getUserRepositories, repositories will be mutated and the view will be updated to reflect the change. Our component should now look like this:

    1. // src/components/UserRepositories.vue
    2. import { fetchUserRepositories } from '@/api/repositories'
    3. import { ref } from 'vue'
    4. export default {
    5. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
    6. props: {
    7. user: { type: String }
    8. },
    9. setup (props) {
    10. const repositories = ref([])
    11. const getUserRepositories = async () => {
    12. repositories.value = await fetchUserRepositories(props.user)
    13. }
    14. return {
    15. repositories,
    16. getUserRepositories
    17. }
    18. },
    19. data () {
    20. return {
    21. filters: { ... }, // 3
    22. searchQuery: '' // 2
    23. }
    24. },
    25. computed: {
    26. },
    27. watch: {
    28. user: 'getUserRepositories' // 1
    29. },
    30. methods: {
    31. updateFilters () { ... }, // 3
    32. },
    33. mounted () {
    34. this.getUserRepositories() // 1
    35. }
    36. }

    We have moved several pieces of our first logical concern into the setup method, nicely put close to each other. What’s left is calling getUserRepositories in the mounted hook and setting up a watcher to do that whenever the user prop changes.

    We will start with the lifecycle hook.

    To make Composition API feature-complete compared to Options API, we also need a way to register lifecycle hooks inside setup. This is possible thanks to several new functions exported from Vue. Lifecycle hooks on composition API have the same name as for Options API but are prefixed with on: i.e. mounted would look like onMounted.

    These functions accept a callback that will be executed when the hook is called by the component.

    Let’s add it to our setup function:

    1. // src/components/UserRepositories.vue `setup` function
    2. import { fetchUserRepositories } from '@/api/repositories'
    3. import { ref, onMounted } from 'vue'
    4. // in our component
    5. setup (props) {
    6. const repositories = ref([])
    7. const getUserRepositories = async () => {
    8. repositories.value = await fetchUserRepositories(props.user)
    9. }
    10. onMounted(getUserRepositories) // on `mounted` call `getUserRepositories`
    11. return {
    12. repositories,
    13. getUserRepositories
    14. }
    15. }

    Now we need to react to the changes made to the user prop. For that we will use the standalone watch function.

    • A Reactive Reference or getter function that we want to watch
    • A callback
    • Optional configuration options

    Here’s a quick look at how it works.

    1. import { ref, watch } from 'vue'
    2. const counter = ref(0)
    3. watch(counter, (newValue, oldValue) => {
    4. console.log('The new counter value is: ' + counter.value)
    5. })

    Whenever counter is modified, for example counter.value = 5, the watch will trigger and execute the callback (second argument) which in this case will log 'The new counter value is: 5' into our console.

    Below is the Options API equivalent:

    1. export default {
    2. data() {
    3. return {
    4. counter: 0
    5. }
    6. },
    7. watch: {
    8. counter(newValue, oldValue) {
    9. console.log('The new counter value is: ' + this.counter)
    10. }
    11. }
    12. }

    For more details on watch, refer to our in-depth guide.

    Let’s now apply it to our example:

    You probably have noticed the use of toRefs at the top of our setup. This is to ensure our watcher will react to changes made to the user prop.

    With those changes in place, we’ve just moved the whole first logical concern into a single place. We can now do the same with the second concern – filtering based on searchQuery, this time with a computed property.

    Similar to ref and watch, computed properties can also be created outside of a Vue component with the computed function imported from Vue. Let’s get back to our counter example:

    1. import { ref, computed } from 'vue'
    2. const counter = ref(0)
    3. const twiceTheCounter = computed(() => counter.value * 2)
    4. counter.value++
    5. console.log(counter.value) // 1
    6. console.log(twiceTheCounter.value) // 2

    Here, the computed function returns a read-only Reactive Reference to the output of the getter-like callback passed as the first argument to computed. In order to access the value of the newly-created computed variable, we need to use the .value property just like with ref.

    Let’s move our search functionality into setup:

    1. // src/components/UserRepositories.vue `setup` function
    2. import { fetchUserRepositories } from '@/api/repositories'
    3. import { ref, onMounted, watch, toRefs, computed } from 'vue'
    4. // in our component
    5. setup (props) {
    6. // using `toRefs` to create a Reactive Reference to the `user` property of props
    7. const { user } = toRefs(props)
    8. const repositories = ref([])
    9. const getUserRepositories = async () => {
    10. repositories.value = await fetchUserRepositories(user.value)
    11. }
    12. onMounted(getUserRepositories)
    13. // set a watcher on the Reactive Reference to user prop
    14. watch(user, getUserRepositories)
    15. const searchQuery = ref('')
    16. const repositoriesMatchingSearchQuery = computed(() => {
    17. return repositories.value.filter(
    18. )
    19. })
    20. return {
    21. repositories,
    22. getUserRepositories,
    23. searchQuery,
    24. repositoriesMatchingSearchQuery
    25. }
    26. }

    We could do the same for other logical concerns but you might be already asking the question – Isn’t this just moving the code to the setup option and making it extremely big? Well, that’s true. That’s why before moving on with the other responsibilities, we will first extract the above code into a standalone composition function. Let’s start with creating useUserRepositories:

    1. // src/composables/useUserRepositories.js
    2. import { fetchUserRepositories } from '@/api/repositories'
    3. import { ref, onMounted, watch, toRefs } from 'vue'
    4. export default function useUserRepositories(user) {
    5. const repositories = ref([])
    6. const getUserRepositories = async () => {
    7. repositories.value = await fetchUserRepositories(user.value)
    8. }
    9. onMounted(getUserRepositories)
    10. watch(user, getUserRepositories)
    11. return {
    12. repositories,
    13. getUserRepositories
    14. }
    15. }

    And then the searching functionality:

    1. // src/composables/useRepositoryNameSearch.js
    2. import { ref, onMounted, watch, toRefs } from 'vue'
    3. export default function useRepositoryNameSearch(repositories) {
    4. const searchQuery = ref('')
    5. const repositoriesMatchingSearchQuery = computed(() => {
    6. return repositories.value.filter(repository => {
    7. return repository.name.includes(searchQuery.value)
    8. })
    9. })
    10. return {
    11. searchQuery,
    12. repositoriesMatchingSearchQuery
    13. }
    14. }

    Now having those two functionalities in separate files, we can start using them in our component. Here’s how this can be done:

    At this point you probably already know the drill, so let’s skip to the end and migrate the leftover filtering functionality. We don’t really need to get into the implementation details as it’s not the point of this guide.

    1. // src/components/UserRepositories.vue
    2. import { toRefs } from 'vue'
    3. import useUserRepositories from '@/composables/useUserRepositories'
    4. import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
    5. import useRepositoryFilters from '@/composables/useRepositoryFilters'
    6. export default {
    7. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
    8. props: {
    9. user: { type: String }
    10. },
    11. setup(props) {
    12. const { user } = toRefs(props)
    13. const { repositories, getUserRepositories } = useUserRepositories(user)
    14. const {
    15. searchQuery,
    16. repositoriesMatchingSearchQuery
    17. } = useRepositoryNameSearch(repositories)
    18. const {
    19. filters,
    20. updateFilters,
    21. filteredRepositories
    22. } = useRepositoryFilters(repositoriesMatchingSearchQuery)
    23. return {
    24. // Since we don’t really care about the unfiltered repositories
    25. // we can expose the end results under the `repositories` name
    26. repositories: filteredRepositories,
    27. getUserRepositories,
    28. searchQuery,
    29. filters,
    30. updateFilters
    31. }
    32. }

    And we are done!

    Keep in mind that we’ve only scratched the surface of Composition API and what it allows us to do. To learn more about it, refer to the in-depth guide.