DEV Community

Cover image for Good practices and Design Patterns for Vue Composables
Jakub Andrzejewski
Jakub Andrzejewski

Posted on • Edited on

Good practices and Design Patterns for Vue Composables

I recently had a great discussion with my team at Vue Storefront about patterns for writing Vue composables. In the case of our system, composables are responsible for storing the main business logic (like calculations, actions, processess) so they are a crucial part of the application. Unfortunately over the time, we didn't have that much time to create some sort of Contract for writing Composables and because of that few of our composables are not really composables 😉

I am really happy that right now we have this time to refactor our approach to building new composables so that they are maintainable, easy to test, and actually useful.

In this article, I will summarise ideas that we have created and also merge them with good practices and design patterns that I read about in few articles.

So this article will be divided into three sections:

  1. General Design Patterns
  2. My recommendations
  3. Further reading

Enjoy and also, let me know what patterns and practices you are using in your projects 🚀

General Design Patterns

The best source in my opinion to learn about patterns for building composables is actually a Vue.js Documentation that you can check out here

Basic Composable

Vue documentation shows following example of a useMouse composable:

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// by convention, composable function names start with "use"
export function useMouse() {
  // state encapsulated and managed by the composable
  const x = ref(0)
  const y = ref(0)

  // a composable can update its managed state over time.
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // a composable can also hook into its owner component's
  // lifecycle to setup and teardown side effects.
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // expose managed state as return value
  return { x, y }
}
Enter fullscreen mode Exit fullscreen mode

That can be later used in the component like following:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
Enter fullscreen mode Exit fullscreen mode

Async composables

For fetching data, Vue recommends following composable structure:

import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  watchEffect(() => {
    // reset state before fetching..
    data.value = null
    error.value = null
    // toValue() unwraps potential refs or getters
    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  })

  return { data, error }
}
Enter fullscreen mode Exit fullscreen mode

That can be then used in the component like following:

<script setup>
import { useFetch } from './useFetch.ts'

const { data, error } = useFetch('...')
</script>
Enter fullscreen mode Exit fullscreen mode

Composables contract

Based on the examples above, here is the contract that all composables should follow:

  1. Composable file names should start with use for example useSomeAmazingFeature.ts
  2. It can accept input arguments that can be primitive types like strings or can accept refs and getters but it requires to use toValue helper
  3. Composable should return a ref value that can be accessed after destructuring the composable like const { x, y } = useMouse()
  4. Composables can hold global state that can be access and modified across the app.
  5. Composable can cause side effects such as adding window event listeners but they should be cleaned when the component is unmounted.
  6. Composables should only be called in <script setup> or the setup() hook. They should also be called synchronously in these contexts. In some cases, you can also call them in lifecycle hooks like onMounted().
  7. Composables can call other composables inside.
  8. Composables should wrap certain logic inside and when too complex, they should be extracted into separate composables for easier testing.

My recommendations

I have built multiple composables for my work projects and Open Source projects - NuxtAlgolia, NuxtCloudinary, NuxtMedusa, so based on these, I would like to add few points to the contract above that are based on my experience.

Stateful or/and pure functions Composables

At certain point of code standarization, you may come into a conclusion that you would like to make a decision about the state hold in the composables.

The easiest functions to test are those who do not store any state (i.e. they are simple input/output functions), for example a composable that would be responsible for converting bytes to human readable value. It accepts a value and returns a different value - it doesn't store any state.

Don't get me wrong, you don't have to make a decision OR. You can completely keep both stateful and stateless composables. But this should be a written decision so that it is easier to work with them later on 🙂

Unit tests for Composables

We wanted to implement unit tests with Vitest for our Frontend application. When working in the backend, having unit tests code coverage is really useful because there you mainly focus on the logic. However, on the frontend you usually work with visuals.

Because of that, we decided that unit testing whole components may not be the best idea because we will be basically unit testing the framework itself (if a button was pressed, check if a state changed or modal opened).

Thanks to the fact that we have moved all the business logic inside the composables (which are basically TypeScript functions) they are very easy to test with Vitest and allows us also to have more stable system.

Scope of Composables

Some time ago, in VueStorefront we have developed our own approach to composables (way before they were actually called like that actually 😄). In our approach, we have beed using composables to map business domain of E-Commerce like following:

const { cart, load, addItem, removeItem, remove, ... } = useCart() 
Enter fullscreen mode Exit fullscreen mode

This approach was definitely useful as it allowed to wrap the domain in one function. And in the simpler examples such as useProduct or useCategory this was relatively simple to implement and maintain. However, as you can see here with the example of useCart when wrapping a domain that contains much more logic than just data fetching, this composable was growing into a shape that was really difficult to develop and maintain.

At this point, I started contributing into Nuxt ecosystem where different approach was introduced. In this new approach, each composable is responsible for one thing only. So instead of building a huge useCart composable, the idea is to build composables for each functionality i.e. useAddToCart, useFetchCart, useRemovefromCart, etc.

Thanks to that, it should be much easier to maintain and test these composables 🙂

Further reading

That will be all from my research. If you would like to learn more about this topic, make sure to check out the following articles:

Top comments (17)

Collapse
 
asedas profile image
asedas • Edited

Hi @jacobandrewsky, good article. I'm Vue/Nuxt developer quite long time, but I found that in most advanced cases those patterns are not enough and I miss one thing here. Especially when you share composable across multiple components (for code reusability ofc), it turns out that often definition and declarations of variables/functions are inside useComposable() function, not exported outside it. Like in example (it's trivial but you will get the point):

// this function should be outside of exported function useMouse()
function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

export function useMouse() {
  // depends on the usecase those refs could be also moved out
  const x = ref(0)
  const y = ref(0)

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}
Enter fullscreen mode Exit fullscreen mode

why is that? because each time we use composable, it runs whole function - so allocates memory for declared functions/variables inside and computes values for declarations, etc. By moving them outside composable function scope we make them reusable across all invokes of composable function which makes this a bit more memory optimized and faster. Ofc not everything can be externalized, but in most projects I've used composables there were quite lot such stuff which in general makes a difference in total. What do you think about such pattern?

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Very good recommendation, thanks for sharing!

Collapse
 
aleksandr_lukianchikov_5c profile image
Aleksandr Lukianchikov

Good idea =)

  • we save resources
  • we separate stateful and stateless logic
  • forces us to use of pure functions
Collapse
 
asedas profile image
asedas

One update: I know about possible closure memory leak due to this externalization but I'm talking about case where we do this intentionally.

Collapse
 
arfedulov profile image
Artem Fedulov

Hi! Good problem you've touched in this post. I've been trying to find a proper way of writing composables for a year or two. So far i came to a conclusion that when we work with a global app state, it's better to use some state management library (e.g. pinia). To me composables are functions that create instances with internal state. There are might be a lot of instances of the same composable and that's fine until these instances start affecting some global state (e.g. useState in Nuxt). So when we start touching global state in a composable, it's probably time to use a state management library. So, shortly speaking, composables are for non-singleton instances, and state manager is for singleton instances. This is just my approach to composables. For sure there are different approaches.

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey Artem,

Thanks for this comment. I think that you can use composables for global state, but as you mentioned, there are already very good solutions like Pinia that will allow you to probably work with global state in a better way :)

Collapse
 
mahmoudabdulmuty profile image
Mahmoud Abdulmuty

"Hi Jakob! Thanks for sharing this informative post about Vue Composables. I found it really helpful! 🙌

I have a question regarding the difference between using vue-composable and Pinia. Could you please shed some light on how these two libraries vary in terms of their primary purposes and functionalities? I'm trying to understand which one would be a better fit for my Vue.js projects.

Thanks in advance, and keep up the great work! 😊"

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey hey!

I am glad that you liked the article. I have not used the vue-composable yet but I have used both Pinia and normal Vue Composables so I can compare them.

In the past, I was using Vuex a lot (the ancestor of Pinia) but with the introduction of Composables, you can manage global state there easily. I dont use Pinia anymore as everything I needed I can do right now with a composable. I just need to register a ref before the composable and then create some sort of getter so that the value can be easily accessible from anywhere around.

But maybe someone else will be able to share more light on this vue-composable package :)

Collapse
 
franciskisiara profile image
Kisiara Francis

In both examples from the Vue documentation, it is clear that the core team has named their files mouse.js and fetch.js respectively. But you suggest that filenames be prefixed with 'use' and then follow a camelCase naming convention. Why so? Why not stick to the example provided, after all the function name itself already describes the contract.

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Hey, good question!

This is something that me and my team come up with.

It is easier for us to search for a file named useComposable than to search through the whole project for the query useComposable. Why? Because you may have several occurences of this composable in your app while you will have only one file.

Also, Nuxt framework uses this approach for naming files nuxt.com/docs/guide/directory-stru...

This rule is more like a matter of preference. This is what worked for me and my team but does not have to necessarily work for you the same way. It is a recommendation rather than strict rule :)

Collapse
 
mannu profile image
Mannu

I am thinking of learning vue. what do u guys say

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Heyo,

I would recommend to do that. It is a great piece of code that allows you to write efficient applications very easily!

Collapse
 
marcelorafaelfeil profile image
Marcelo Rafael • Edited

Good article, thanks for that.
I have a question, maybe you can help me with your opinion.
If I would like to execute some action right after I include a product into the cart, what do you suggest?

I thought to write a function into the composable named as onAdd that will receive another function that will be called after a product be included into the cart. i.e:

export const useCart = () => {
  const cart = [];
  let onAddCartCallback = undefined;

  const addProduct = () => {
    /* ... */
    if (onAddCartCallback) {
      onAddCartCallback();
    }
  }

  const onAdd = (callback) => {
    onAddCartCallback = callback;
  }

  return { cart, addProduct, onAdd };
}
Enter fullscreen mode Exit fullscreen mode

What do you think about this approach?

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

I would recommend maybe adding a watcher over the cart element. Once cart changes (for example becomes truthy) let's automatically call another function (i.e. display notification). This way, you would trigger this function everytime the cart will be updated and you wouldn't need to do it in other places :)

Collapse
 
lorenaramonda profile image
Lorena Ramonda

Hello @jacobandrewsky there's a thing that it's a little confusing to me.
In the paragraph called Composables contract you say:

Composable file names should start with use for example useSomeAmazingFeature.ts

but then in example right above you write:

import { useFetch } from './fetch.js'

Wouldn't it be better to reflect in the example the rules that you are suggesting to avoid misunderstanding? 😁

Or were you meaning something else?

Collapse
 
jacobandrewsky profile image
Jakub Andrzejewski

Heyo, good catch!

That is indeed correct. I think I have copied the useFetch example from the official documentation.

Will fix that in a sec! :)

Collapse
 
merline profile image
merline

Great read on Vue Composables! The article outlines essential practices and design patterns for efficient Vue development. Speaking of seamless transitions, I highly recommend checking out Day-to-Night by homery.design/services/day-to-night/. Their service brilliantly aligns with Vue's flexibility, offering a smooth day-to-night transition solution. It's a valuable addition to consider for enhancing user experience and design aesthetics. #VueJS #WebDevelopment #DesignPatterns