I first saw this done by Eduardo (aka posva) way back in 2018 at a workshop in Toronto. So thanks Eduardo, you’re the inspiration for this one!
Here’s the gist of this pattern (although “pattern” is maybe quite loosely applied here):
We want to first figure out how this new composable is going to be used. Once we do that, it’s much easier to figure out the implementation.
In this article, I’ll expand on what this means exactly, how you can do it, and then show you an example of this in practice.
Note: this process works well with almost any type of code, not just composables. It’s just the example I’m using in this article.
The key to doing this is actually surprisingly simple:
We use the composable as if it already exists.
Instead of implementing the composable, then using it in our components, we flip this around. We first “use” the composable in our components the way we’d want it to work, and then we implement it to match that interface.
We wave our magic wand 🪄 and imagine the world with all of our problems solved, a million dollars in the bank, and sunny beautiful weather outside.
Now that everything is perfect, we’ll ask ourselves a few questions:
ref
, a raw value, an Array, or maybe an Object? What is the shape/type of that Object?useAsyncData
does?Let me show you a quick example of how this works in practice.
If we needed to get the mouse movement from a composable, the canonical composable example, we’d first use it in our component:
<template><p>X: {{ x }}</p><p>Y: {{ y }}</p></template><script setup>const { x, y } = useMousePos();</script>
That’s our first stab at how it might work, but can we do better?
Maybe we’ll eventually want more data from this composable, so we should stick x
and y
into an object to keep them together:
<template><p>X: {{ position.x }}</p><p>Y: {{ position.y }}</p></template><script setup>const { position } = useMousePos();</script>
But now we’ll have to call this something other than useMousePos
, because we’ll have more than just the mouse position:
<template><p>X: {{ position.x }}</p><p>Y: {{ position.y }}</p></template><script setup>const { position } = useMouse();</script>
Okay, cool!
Then, we remember that we need to be able to dynamically update the cursor. That’s usually a CSS thing, so maybe it doesn’t make sense to do through this composable, but let’s at least try out the interface to see if it “feels” right:
<template><p>X: {{ position.x }}</p><p>Y: {{ position.y }}</p></template><script setup>const { position, setCursor } = useMouse();onMounted(() => {setTimeout(() => {setCursor('pointer');}, 2000);});</script>
Okay, now we’re getting somewhere!
How do we implement setCursor
? Well, that’s not the point of this exercise. The point is that we’ve now iterated on the interface, before writing any implementation.
On more complex composables iterating on the interface first like this can save you a lot of time. Seriously.
Sometimes you only realize after trying to use the composable that you want to pass in an Array, not an Object, or that some other type doesn’t make sense. By doing it this way, you do all of that learning up front, before you’ve spent (really, wasted) a lot of your time on implementing it the wrong way.
It’s possible to spend too much time on this, trying to think of every possible scenario and use case. This goes by several names: YAGNI, gold plating, or overengineering.
The point is, it’s possible to spend too much time designing things up front. The trick is to find that balance (which can be hard). Usually though, this happens because we get stuck in our heads thinking of all possibilities. If instead, we start with coding the interface like I showed here, we can more easily avoid this.
We’re only writing the interface based on what’s actually needed from our code, and nothing more.
Trust me, once you’ve spent weeks building a feature that no one ever uses, you’ll learn the importance of this. I’ve written far too much useless code in my life…
When writing your composables, first start with how you’re going to use them — the interface!
It flips the typical development process around, but makes it more likely that you’ll build the right thing the first time, not after lots and lots of iteration.
The best part about this pattern is that you can apply it to all types of software, not just composables. In fact, this is a principle that extends beyond software and works in many areas of life.
Start with the end in mind, and work backwards from there.