Vue Composition API: Composables

  You block advertising 😢
Would you like to buy me a ☕️ instead?

What makes the Vue 3 Composition API so much better than the Options API is code sharing. Inside the setup hook of a component, we can group parts of our code by logical concern. We then can extract pieces of reactive logic and share the code with other components.

A pattern is emerging to call such pieces of reusable reactive code Composables. Composables are similar to what is called Hooks in the React world. With Vue 2, we only had Mixins for making code reusable across components. Composables are a much more powerful and transparent approach for sharing code between components.

Using Composables to Share Reactive Code

Typically, Composables are functions that return one or multiple reactive variables (typically created with ref, reactive, or computed) and methods to manipulate the reactive data.

// src/composables/counter.js
import { computed, ref } from 'vue';

export function useCounter() {
  const count = ref(0);
  const next = computed(() => count.value + 1);
  const plusOne = () => { count.value += 1 };
  
  return {
    count,
    next,
    plusOne,
  };
}

Here we can see a very simple useCounter() Composable. We can use it in our components like that:

<!-- src/components/MyComponent.vue -->
<template>
  <div>Count: {{ count }} (next: {{ next }})</div>
  <button @click="plusOne">
    +1
  </button>
</template>

<script>
import { useCounter } from '../composables/counter';

export default {
  setup() {
    const {
      count,
      next,
      plusOne,
    } = useCounter();
  
    return {
      count,
      next,
      plusOne,
    };
  },
};
</script>

In this case we only use a single useCounter() Composable in our component but we can also combine multiple Composables in a single component.

Don’t Shoot Yourself in the Foot: Use readonly()!

Our useCounter() Composable has a problem: its internal state is mutable by consumers. Most of the time, we should avoid allowing to change the state of a Composable directly from a component. Otherwise, it can become very complicated to debug components and Composables because it is not always clear how and when the state is changed.

// src/composables/counter.js
import { computed, readonly, ref } from 'vue';

export function useCounter() {
  const count = ref(0);
  const next = computed(() => count.value + 1);
  const plusOne = () => { count.value += 1 };
  
  return {
    count: readonly(count),
    next,
    plusOne,
  };
}

Luckily we can use the readonly() function to mark refs and reactive objects as readonly. Now the only way for consumer components to mutate the count state is by calling the plusOne() method. If you’re familiar with Vuex than you might notice that this is similar to how we must use mutations to mutate the state of a Vuex store. This is pretty much the same pattern.


Do you want to learn more about advanced Vue.js techniques?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.


Don’t Shoot Yourself in the Foot: Use toRefs()!

Another common mistake is to use object destructuring with reactive values.

// ❌
function useReactiveObject() {
  const state = reactive({ foo: 'bar' });

  return {
    setFoo: newValue => { state.foo = newValue },
    state,
  };
}

const { state, setFoo } = useReactiveObject();
const { foo } = state;
console.log(foo); // `bar`

setFoo('baz');
console.log(foo); // `bar` <- did not update!

Here we use destructuring to get foo from the reactive state object returned by useReactiveObject(). By doing so, we lose the reactive properties of foo! Changing the value of foo on the reactive object will not update the destructured foo variable.

Instead, we can use toRefs() to convert all the properties of the state object to refs:

// ✅
function useReactiveObject() {
  const state = reactive({ foo: 'bar' });

  return {
    setFoo: newValue => { state.foo = newValue },
    state: toRefs(state),
  };
}

const { state, setFoo } = useReactiveObject();
const { foo } = state;
console.log(foo.value); // `bar`

setFoo('baz');
console.log(foo.value); // `baz` <- updated!

Make sure never to use destructuring on reactive objects. This is also true for reactive objects created with ref:

// ❌
const refObject = ref({ foo: 'bar' });
const { foo } = refObject.value; // `foo` is not reactive!

The Singleton State Pattern

The useCounter() Composable uses local state. Every time we call useCounter() in one of our components, we create a new count instance. So when we use useCounter() in two different components, each of them has its count state and calling plusOne() in one component has no effect on the count of the other component.

But what if we want that to be the case? What if we want to have a global count state that we can update from any component of our application? Then we can use the Singleton State Pattern.

// src/composables/counter.js
import { computed, readonly, ref } from 'vue';

const count = ref(0);
const next = computed(() => count.value + 1);
const plusOne = () => { count.value += 1 };

export function useCounter() {
  return {
    count: readonly(count),
    next,
    plusOne,
  };
}

As you see above, it’s very straightforward to create a shared global state with the Composition API. By moving our reactive code outside of the useCounter() function, we now only instantiate the count state a single time, when the module is loaded for the first time. Calling useCounter() only returns a reference to the global count, next, and plusOne instances.

Now when we call plusOne() in one component, we also update the count state of all the other components using useCounter().

Use Existing Composables

We don’t have to reinvent the wheel every time we build a new feature. Because Composables make it incredibly easy to share code, there already is an extensive library of high-quality Composables called VueUse, that we can use for our projects.

Further Reading


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi