Coding Better Composables: Dynamic Returns (3/5)

What if your composable could change what is returned based on its use? If you only needed a single value, it could do that. And if you needed a whole object returned, it could do that, too.

This article will look at a pattern for adding dynamic returns to composables. We’ll understand when to use this pattern, how to implement it, and look at some examples of the pattern in use.

This is the third article in a five-part series on coding better Vue.js composables. If you’ve missed the first two, here’s the link to start from the beginning. By exploring this series, you’ll have a clear understanding of crafting solid composables while using best practices.

Here’s what this series covers:

  1. How to use an options object parameter to make your composables more configurable
  2. Using the ref and unref to make our arguments more flexible
  3. A simple way to make your return values more useful 👈  we’re here
  4. Why starting with the interface makes your composables more robust
  5. How to use async code without the need for await — making your code easier to understand

Before we get started, we need to make sure we’re on the same page. So let’s first look at what a composable is.


What is a Composable?

According to the Vue documentation, a composable is “a function that leverages Vue Composition API to encapsulate and reuse stateful logic”.

This means that any code that uses reactivity can be turned into a composable.

Here’s a simple example of a useMouse composable from the docs:

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

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

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

  return { x, y }
}

We define our state as refs, then update that state whenever the mouse moves. By returning the x and y refs, we can use them inside of any component (or even another composable).

Here’s how we use this composable inside of a component:

<template>
  X: {{ x }} Y: {{ y }}
<script setup>
  import { useMouse } from './useMouse';
  const { x, y } = useMouse();
</script>

We’ve seen what a composable is and why it’s important. Now let’s look at our third pattern for writing better composables.


The Pattern of Dynamic Return Values

This pattern continues in the “why not have it both ways?” line of thinking from the previous article on flexible arguments.

A composable can either return a single value, or an object of values:

// Returns a single value
const isDark = useDark();

// Returns an object of values
const {
  counter,
  pause,
  resume,
} = useInterval(1000, { controls: true });

(I guess you could also return an array of values, but why would you do that?)

You can also dynamically switch between the two. For example, return a single value when that’s all you need, or return a whole object when you need more control and granularity.

This is a nice feature to have because you can control what level of complexity you want. Simple when all you need is simplicity. Complex when what you need is complexity.

The useInterval composable from VueUse illustrates this perfectly.

Most of the time, when using useInterval, you only need the counter. So by default, it just returns that:

// Default behaviour
const counter = useInterval(1000);

// 1...
// 2...
// 3...

However, if you wanted to pause and resume the counter, you can get it to do that as well. You can do this by adding in the controls option:

// Even more control!
const {
  counter,
  pause,
  resume,
} = useInterval(1000, { controls: true });

// 1...
// 2...
pause();
// ...
resume();
// 3...
// 4...

So now let’s see how we can use this pattern of dynamic return values in our composables.


Implementing in a composable

To implement this pattern, we need to do two things:

  1. Add an option in our options object that turns it on
  2. Use that option to change the behavior of our return statement

Here’s a simple sketch of what this looks like:

export default useComposable(input, options) {
  // 1. Add in the `controls` option
  const { controls = false } = options;
  
  // ...

  // 2. Either return a single value or an object
  if (controls) {
    return { singleValue, anotherValue, andAnother };
  } else {
    return singleValue;
  }
}

How you decide to do this switching is ultimately up to you. Do what makes the most sense for you and your composable.

Perhaps you’ll want to switch on an existing option instead of using a controls option only for this purpose. Maybe it will be cleaner to use a ternary in the return instead of an if statement. Or maybe there’s an entirely different way that works best for you. The important thing with this pattern is the switching, not how the switching works.

Next, let’s see how some composables from VueUse implement this pattern.

VueUse is an open-source collection of composables for Vue 3 and is well written. It’s a fantastic resource for learning to write better composables!


useInterval

First, let’s take a deeper look at how useInterval works.

This composable will update a counter on every interval:

// Updates `counter` every 500 milliseconds
const counter = useInterval(500);

At the very top of the composable we destructure our options object, pulling out the controls option and renaming it to exposeControls. By default we won’t show the controls:

const {
  controls: exposeControls = false,
  immediate = true,
} = options;

Then, at the end, we have an if statement that switches on exposeControls. Either we return an object that includes the counter ref and all of the controls, or just the counter ref:

if (exposeControls) {
  return {
    counter,
    ...controls,
  };
else {
  return counter;
}

The extra controls come from a helper composable used by the useInterval composable. We’ll see this being used again in the next composable.

With these two code snippets, we can make any composable have a more flexible return statement.

Now let’s take a look at the useNow composable.


useNow

The useNow composable lets us grab a Date object that represents now and updates reactively:

const now = useNow();

By default, it will refresh itself every frame — typically 60 times per second. We can change how often it updates, but we can also pause and resume the update process:

const { now, pause, resume } = useNow({ controls: true });

This composable works in a very similar way to the useInterval composable. Internally they both use the useIntervalFn helper that VueUse exposes.

First, we destructure the options object to get the controls option, again renaming it to exposeControls to avoid a naming collision:

const {
  controls: exposeControls = false,
  interval = 'requestAnimationFrame',
} = options;

Then we return at the end of the composable. Here we use an if statement to switch between the two cases:

if (exposeControls) {
  return {
    now,
    ...controls,
  };
else {
  return now;
}

As you can see, this pattern is implemented nearly identically in both the useInterval and useNow composables. All of the composables in VueUse that implement this pattern do it in this particular way.

Here’s a list of all the composables — that I could find — that implement this pattern in VueUse, for you to explore more on your own:

  • useInterval
  • useTimeout
  • useNow
  • useTimestamp
  • useTimeAgo

Wrapping Things Up

We saw that dynamic return values let us choose how we want to use the composable more flexibly. We can get a single value back if that’s all we need. And we can also get an entire object with values, methods, and whatever else we might want.

But we didn’t just look at the pattern itself. We saw how the useInterval and useNow composables from VueUse implement this pattern. Either they return a single value, a counter or the current timer, or they can also provide controls for pausing and resuming updates.

This pattern is great for simplifying our code in most cases while still allowing for greater complexity when it’s needed. It’s sort of like a desk with a drawer. You can have lots of stuff laid out on the desk when you need it. But you can also put them away in the drawer to keep things tidy.

In the next article in this series, we’ll learn how to write composables from scratch. We’ll look at starting with the interface in mind, imagining how it will be used, which makes it easier to write a composable that will work well for us far into the future.

Download the cheatsheets

Save time and energy with our cheat sheets.