Creating Custom Composition functions in Vue 3 with the Composition API

Section 1: Understanding Composition Functions

Definition and Purpose of Composition Functions

Composition functions are a core feature of the Composition API in Vue 3. They allow developers to encapsulate and reuse logic across different components in a more modular and organized way. Essentially, a composition function is a JavaScript function that uses Vue’s reactive APIs to create and manage reactive state, computed properties, and lifecycle hooks.

The primary purpose of composition functions is to promote code reuse and separation of concerns. By extracting logic into reusable functions, you can keep your components clean and focused on their primary responsibilities, making your codebase easier to maintain and understand.

As simple as this concept is; I am often surprised how seldom I see them being used in every day coding. Hence this blog post with the hope that it encourages you to adopt this style of encapsulation into your projects.

Benefits of Using Composition Functions in Vue 3

  1. Modularity: Composition functions enable you to break down complex logic into smaller, reusable pieces. This modularity makes your code more manageable and easier to test.
  2. Reusability: Logic encapsulated in composition functions can be reused across multiple components, reducing code duplication and improving consistency.
  3. Separation of Concerns: By separating logic from the component’s template and lifecycle hooks, you can achieve a clearer separation of concerns, leading to more maintainable code.
  4. Improved Readability: With composition functions, you can organize related logic together, making your code more readable and easier to follow.
  5. Enhanced Flexibility: The Composition API provides more flexibility compared to the Options API, allowing you to use JavaScript features like closures and higher-order functions to create more powerful abstractions.

Examples of Common Use Cases

  1. Form Handling: You can create a composition function to manage form state, validation, and submission logic, which can then be reused across different forms in your application.
  2. Data Fetching: Encapsulate data fetching logic in a composition function to handle API calls, loading states, and error handling. This function can be reused in any component that needs to fetch data.
  3. Authentication: Create a composition function to manage user authentication state, login, and logout logic. This can be shared across components that need to access authentication status.
  4. Responsive Design: Use a composition function to handle responsive design logic, such as detecting screen size changes and updating component state accordingly.
  5. Event Handling: Encapsulate event handling logic, such as debouncing or throttling user input, in a composition function to ensure consistent behavior across components.

By understanding and leveraging composition functions, you can significantly enhance the modularity, reusability, and maintainability of your Vue 3 applications. In the next sections, we’ll dive deeper into how to create and use these powerful functions in your projects.

Section 2: Setting Up Your Vue 3 Project

Prerequisites

Before diving into creating custom composition functions, ensure you have the following prerequisites installed on your development environment:

  • Node.js: Make sure you have Node.js installed. You can download it from the official Node.js website.
  • Vue CLI: The Vue CLI is a powerful tool for scaffolding and managing Vue projects. Install it globally using npm:
npm install -g @vue/cli

Creating a New Vue 3 Project

With the prerequisites in place, you can now create a new Vue 3 project. Follow these steps:

Open your terminal and navigate to the directory where you want to create your project.
Run the Vue CLI command to create a new project:

vue create my-vue3-project

Select the default preset or manually select features. For this tutorial, the default preset is sufficient, but feel free to customize based on your needs.
Navigate into your project directory:

cd my-vue3-project

Installing Necessary Dependencies

To make the most out of Vue 3 and the Composition API, you might need to install additional dependencies. Here are some commonly used ones:

1. Vue Router: For handling routing in your application.

npm install vue-router@next

2. Vuex (optional): For state management, although with the Composition API, you might manage state differently.

npm install vuex@next

3. Axios: For making HTTP requests.

npm install axios

With your project set up and dependencies installed, you’re ready to start building with Vue 3 and the Composition API. In the next section, we’ll dive into the basic structure of a composition function and how to create your first custom function.

Section 3: Basic Structure of a Composition Function

Anatomy of a Composition Function

A composition function in Vue 3 is essentially a JavaScript function that encapsulates reactive state, methods, computed properties, and lifecycle hooks. The goal is to create reusable logic that can be easily integrated into Vue components. Here’s a breakdown of the key elements:

  1. Reactive State: Using Vue’s reactive or ref functions to create reactive data.
  2. Methods and Computed Properties: Defining functions and computed properties to manipulate and derive state.
  3. Lifecycle Hooks: Using Vue’s lifecycle hooks within the composition function to manage side effects.

Reactive State

Reactive state is the cornerstone of any composition function. You can create reactive state using the reactive or ref functions provided by Vue 3.

  • reactive: Creates a reactive object.
  import { reactive } from 'vue'

  function useCounter() {
    const state = reactive({
      count: 0
    })

    return { state }
  }
  • ref: Creates a reactive reference to a primitive value.
  import { ref } from 'vue'

  function useCounter() {
    const count = ref(0)

    return { count }
  }

Methods and Computed Properties

Methods are functions that perform actions, while computed properties are derived from reactive state and automatically update when dependencies change.

  • Methods:
  function useCounter() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    return { count, increment }
  }
  • Computed Properties:
  import { computed } from 'vue'

  function useCounter() {
    const count = ref(0)

    const doubleCount = computed(() => count.value * 2)

    return { count, doubleCount }
  }

Lifecycle Hooks

You can use Vue’s lifecycle hooks within composition functions to manage side effects. For example, you might want to fetch data when a component is mounted.

import { onMounted, ref } from 'vue'

function useFetchData(url) {
  const data = ref(null)
  const error = ref(null)

  onMounted(async () => {
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    }
  })

  return { data, error }
}

Example: Creating a Simple Composition Function

Let’s put it all together and create a simple composition function that manages a counter with reactive state, a method to increment the counter, and a computed property to double the count.

import { ref, computed } from 'vue'

function useCounter() {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  const doubleCount = computed(() => count.value * 2)

  return { count, increment, doubleCount }
}

export default useCounter

In this example, useCounter is a composition function that provides a reactive count, an increment method to update the count, and a doubleCount computed property. This function can be imported and used in any Vue component, making it a reusable piece of logic.

By understanding the basic structure of composition functions, you can start creating your own custom functions to encapsulate and reuse logic across your Vue 3 applications. In the next section, we’ll explore more advanced techniques for building powerful composition functions.

Section 4: Advanced Composition Function Techniques

Handling Asynchronous Operations

Asynchronous operations, such as fetching data from an API, are common in modern web applications. The Composition API provides a clean way to handle these operations within composition functions. You can use async/await syntax inside lifecycle hooks or methods to manage asynchronous tasks.

Example:

import { ref, onMounted } from 'vue'

function useFetchData(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  const fetchData = async () => {
    loading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, error, loading, fetchData }
}

In this example, useFetchData handles data fetching, error management, and loading state.

Using Watchers and Refs

Watchers allow you to perform side effects in response to reactive state changes. The Composition API provides the watch and watchEffect functions for this purpose.

Example:

import { ref, watch } from 'vue'

function useCounter() {
  const count = ref(0)
  const message = ref('')

  watch(count, (newCount) => {
    if (newCount > 10) {
      message.value = 'Count is greater than 10!'
    } else {
      message.value = ''
    }
  })

  const increment = () => {
    count.value++
  }

  return { count, message, increment }
}

Here, watch monitors changes to count and updates message accordingly.

Composing Multiple Composition Functions

One of the strengths of the Composition API is the ability to compose multiple composition functions to build complex logic. You can import and use multiple composition functions within a single component or another composition function.

Example:

import { ref, computed } from 'vue'
import useFetchData from './useFetchData'
import useCounter from './useCounter'

function useEnhancedCounter(url) {
  const { data, error, loading, fetchData } = useFetchData(url)
  const { count, increment } = useCounter()

  const doubleCount = computed(() => count.value * 2)

  return { data, error, loading, fetchData, count, increment, doubleCount }
}

export default useEnhancedCounter

In this example, useEnhancedCounter combines useFetchData and useCounter to create a more complex composition function.

Example: Building a More Complex Composition Function

Let’s build a more complex composition function that combines several advanced techniques. This function will manage a list of items, including fetching data, adding new items, and filtering the list.

import { ref, computed, watch } from 'vue'

function useItemList(url) {
  const items = ref([])
  const filter = ref('')
  const loading = ref(true)
  const error = ref(null)

  const fetchItems = async () => {
    loading.value = true
    try {
      const response = await fetch(url)
      items.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  const addItem = (item) => {
    items.value.push(item)
  }

  const filteredItems = computed(() => {
    return items.value.filter(item => item.includes(filter.value))
  })

  watch(filter, (newFilter) => {
    console.log(`Filter changed to: ${newFilter}`)
  })

  return { items, filter, loading, error, fetchItems, addItem, filteredItems }
}

export default useItemList

In this example, useItemList handles fetching items from an API, adding new items to the list, and filtering the list based on a search term. It also demonstrates the use of watchers to log changes to the filter.

By mastering these advanced techniques, you can create powerful and reusable composition functions that significantly enhance the functionality and maintainability of your Vue 3 applications. In the next section, we’ll discuss best practices for creating and using custom composition functions.

Section 5: Best Practices for Custom Composition Functions

Naming Conventions

Adopting clear and consistent naming conventions is crucial for maintaining readability and understanding in your codebase. Here are some guidelines:

  • Prefix with use: Start the name of your composition function with use to indicate that it is a hook. For example, useCounter, useFetchData.
  • Descriptive Names: Use descriptive names that clearly convey the purpose of the function. Avoid abbreviations and overly generic names.
  • Consistency: Stick to a consistent naming pattern across your project to make it easier for other developers to understand and use your functions.

Code Organization and Modularity

Organizing your code effectively enhances maintainability and scalability. Here are some tips:

  • Separate Files: Place each composition function in its own file. This makes it easier to manage and locate specific functions.
  • Logical Grouping: Group related composition functions into directories. For example, you might have a composables directory with subdirectories for different domains like auth, data, ui, etc.
  • Modularity: Keep your composition functions focused and modular. Avoid creating monolithic functions that handle too many responsibilities. Instead, compose smaller functions together.

Example directory structure:

src/
  composables/
    auth/
      useAuth.js
    data/
      useFetchData.js
    ui/
      useToggle.js

Testing Composition Functions

Testing is essential to ensure the reliability and correctness of your composition functions. Here are some best practices:

  • Unit Tests: Write unit tests for your composition functions to verify their behavior in isolation. Use testing libraries like Jest or Vue Test Utils.
  • Mocking: Mock dependencies and external services to test your functions in a controlled environment.
  • Edge Cases: Test edge cases and error scenarios to ensure your functions handle unexpected inputs gracefully.

Example test using Jest:

import { ref } from 'vue'
import { useCounter } from './useCounter'

test('increments count', () => {
  const { count, increment } = useCounter()
  increment()
  expect(count.value).toBe(1)
})

Performance Considerations

Performance is a critical aspect of any application. Here are some tips to optimize the performance of your composition functions:

  • Lazy Initialization: Initialize state and perform expensive operations lazily, only when needed.
  • Avoid Unnecessary Re-renders: Use watch and computed properties wisely to avoid triggering unnecessary re-renders.
  • Debounce and Throttle: Implement debouncing and throttling for functions that are called frequently, such as event handlers.
  • Memory Management: Clean up resources and subscriptions in lifecycle hooks to prevent memory leaks.

Example of debouncing a function:

import { ref } from 'vue'
import { debounce } from 'lodash'

function useSearch() {
  const query = ref('')
  const results = ref([])

  const search = debounce(async (q) => {
    // Perform search operation
    results.value = await fetchResults(q)
  }, 300)

  watch(query, (newQuery) => {
    search(newQuery)
  })

  return { query, results }
}

By following these best practices, you can create robust, maintainable, and performant composition functions that enhance the quality of your Vue 3 applications. In the next section, we’ll explore real-world examples to see these principles in action.

Section 6: Real-World Examples

Example 1: Custom Form Validation Logic

Form validation is a common requirement in web applications. Using the Composition API, you can create a reusable function to handle form validation logic.

import { ref, reactive, computed } from 'vue'

function useFormValidation() {
  const form = reactive({
    name: '',
    email: '',
    password: ''
  })

  const errors = reactive({
    name: '',
    email: '',
    password: ''
  })

  const validateName = () => {
    errors.name = form.name ? '' : 'Name is required'
  }

  const validateEmail = () => {
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    errors.email = emailPattern.test(form.email) ? '' : 'Invalid email address'
  }

  const validatePassword = () => {
    errors.password = form.password.length >= 6 ? '' : 'Password must be at least 6 characters'
  }

  const validateForm = () => {
    validateName()
    validateEmail()
    validatePassword()
  }

  const isFormValid = computed(() => {
    return !errors.name && !errors.email && !errors.password
  })

  return { form, errors, validateForm, isFormValid }
}

export default useFormValidation

In this example, useFormValidation manages form state and validation logic. It provides reactive form fields, error messages, and a method to validate the entire form.

Example 2: Reusable Data Fetching Logic

Data fetching is another common task that can be encapsulated in a composition function. This example demonstrates how to create a reusable data fetching function.

import { ref, onMounted } from 'vue'
import axios from 'axios'

function useFetchData(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  const fetchData = async () => {
    loading.value = true
    try {
      const response = await axios.get(url)
      data.value = response.data
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, error, loading, fetchData }
}

export default useFetchData

Here, useFetchData handles the logic for fetching data from an API. It manages the loading state, error handling, and provides a method to re-fetch the data.

Example 3: Managing Global State without Vuex

While Vuex is a popular choice for state management, you can also manage global state using the Composition API. This example shows how to create a simple global state management solution.

import { reactive, readonly } from 'vue'

const state = reactive({
  user: null,
  isAuthenticated: false
})

const setUser = (user) => {
  state.user = user
  state.isAuthenticated = !!user
}

const clearUser = () => {
  state.user = null
  state.isAuthenticated = false
}

function useAuth() {
  return {
    state: readonly(state),
    setUser,
    clearUser
  }
}

export default useAuth

In this example, useAuth provides a way to manage user authentication state globally. The state is reactive and can be accessed and modified through the provided methods.

By leveraging these real-world examples, you can see how powerful and flexible the Composition API is for managing various aspects of your Vue 3 applications. These examples demonstrate how to encapsulate logic into reusable functions, making your code more modular and maintainable. In the next section, we’ll discuss how to integrate these custom composition functions into your components.

Section 7: Integrating Custom Composition Functions in Components

How to Import and Use Custom Composition Functions in Vue Components

Integrating custom composition functions into your Vue components is straightforward. The process involves importing the composition function and using it within the setup function of your component. This allows you to leverage the reactive state, methods, and computed properties defined in the composition function.

Here’s a step-by-step guide:

  1. Import the Composition Function: Import the custom composition function at the top of your component file.
  2. Use the Composition Function in setup: Call the composition function within the setup function of your component. Destructure the returned values to access the reactive state and methods.
  3. Bind State and Methods to the Template: Use the reactive state and methods in your component’s template as needed.

Example: Integrating a Custom Composition Function into a Component

Let’s integrate the useCounter composition function we created earlier into a Vue component.

Step 1: Import the Composition Function

// CounterComponent.vue
<script setup>
import { ref } from 'vue'
import useCounter from './composables/useCounter'
</script>

Step 2: Use the Composition Function in setup

<script setup>
import { ref } from 'vue'
import useCounter from './composables/useCouter'

const { count, increment, doubleCount } = useCounter()
</script>

Step 3: Bind State and Methods to the Template

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

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

const { count, increment, doubleCount } = useCounter()
</script>

In this example, the useCounter composition function is imported and used within the setup function of CounterComponent.vue. The reactive state (count), method (increment), and computed property (doubleCount) are destructured from the composition function and bound to the component’s template.

This approach keeps your component logic clean and focused, while the reusable logic is encapsulated within the composition function. By following these steps, you can easily integrate any custom composition function into your Vue components, enhancing modularity and reusability.

With this knowledge, you should now be equipped to create, use, and integrate custom composition functions in your Vue 3 projects, making your codebase more maintainable and scalable.

Conclusion

In this article, we’ve explored the powerful capabilities of custom composition functions in Vue 3 using the Composition API. Here’s a quick recap of the key points we’ve covered:

  • Understanding Composition Functions: We defined what composition functions are, their purpose, and the benefits they bring to Vue 3 applications.
  • Setting Up Your Vue 3 Project: We walked through the prerequisites, creating a new Vue 3 project, and installing necessary dependencies.
  • Basic Structure of a Composition Function: We discussed the anatomy of a composition function, including reactive state, methods, computed properties, and lifecycle hooks, with a simple example.
  • Advanced Composition Function Techniques: We delved into handling asynchronous operations, using watchers and refs, composing multiple composition functions, and building a more complex example.
  • Best Practices for Custom Composition Functions: We highlighted naming conventions, code organization, testing, and performance considerations.
  • Real-World Examples: We provided practical examples of custom form validation logic, reusable data fetching logic, and managing global state without Vuex.
  • Integrating Custom Composition Functions in Components: We demonstrated how to import and use custom composition functions within Vue components.

Custom composition functions are a powerful tool that can significantly enhance the modularity, reusability, and maintainability of your Vue 3 applications. I encourage you to experiment with creating your own composition functions to encapsulate and share logic across your projects. The more you practice, the more you’ll discover the flexibility and power of the Composition API.

For further learning, here are some additional resources:

  • Vue 3 Documentation
  • Vue Mastery – Courses and tutorials on Vue.js
  • Vue School – Interactive Vue.js courses
  • Official Vue.js Forum – Community discussions and support

Happy coding, and may your Vue 3 projects thrive with the power of custom composition functions!

i hope you found this guide on creating custom composition functions in Vue 3 helpful and inspiring.

Now it’s your turn!

I’d love to see the custom composition functions you’ve created. Share your examples and experiences in the comments section below.

If you have any questions or need further clarification on any of the topics covered, don’t hesitate to ask. Your feedback and questions are invaluable and help me improve my content.

Let’s learn and grow together as a community.

Let’s Start a Project!

Contact Me