Composables are great, except that it seems we always need to create a new file for them.
I want to explore some ways we can create inline composables — no need to create new files all over the place!
Inline composables help to organize components by creating boundaries within our logic, which is especially helpful in larger components that are tough to refactor.
Many components are small enough and focused enough that they won’t really benefit from this.
You’ll just have to keep reading to see what I mean.
One approach that I discovered — and then subsequently learned was in the docs — is to add computed and methods directly on to a reactive object:
const counter = reactive({ count: 0, increment() { this.count += 1; }, decrement() { this.count -= 1; }, });
This works because this
is set to the object that the method is accessed through, which happens to be the reactive object.
Vue’s reactivity system uses Proxies to watch for when a property is accessed and updated. In this case, we have a small overhead from accessing the method as a property on the object, but it doesn’t trigger any updates.
If we had a whole series of counters we can reuse this over and over:
const listOfCounters = []; for (const i = 0; i < 10; i++) { const counter = reactive({ id: i, count: 0, increment() { this.count += 1; }, decrement() { this.count -= 1; }, }) listOfCounters.push(counter); }
In our template we can use the counters individually:
<div v-for="counter in listOfCounters" :key="counter.id"> <button @click="counter.decrement()">-</button> {{ counter.count }} <button @click="counter.increment()">+</button> </div>
We know that reactive and ref are mostly interchangeable, so we have another approach we can take.
Instead of making the entire object reactive, we can use ref
to make only our state reactive:
const counter = { count: ref(0), increment() { this.count.value += 1; }, decrement() { this.count.value -= 1; }, };
This saves us a small and likely unnoticeable overhead. But it also feels somewhat better since we’re being more thoughtful with our use of reactivity instead of spraying it everywhere.
Here’s our example from before, but this time I’m going to add in a factory function to make it more readable:
const createCounter = (i) => ({ id: i, count: ref(0), increment() { this.count.value += 1; }, decrement() { this.count.value -= 1; }, }); const listOfCounters = []; for (const i = 0; i < 10; i++) { listOfCounters.push(createCounter(i)); }
Of course, we can use a factory method with the previous reactive method as well.
We have two interesting methods for managing state, but they both have a big problem:
They introduce a new and different way of managing state.
Instead, why not stick with a format that we know, but keep it in the same file?
We’ll refactor our previous example to be more like a regular composable:
const useCount = (i) => { const count = ref(0); const increment = () => count.value += 1; const decrement = () => count.value -= 1; return { id: i, count, increment, decrement, }; }; const listOfCounters = []; for (const i = 0; i < 10; i++) { listOfCounters.push(useCount(i)); }
This is a much better solution to me. It’s familiar, yet defined inline so we don’t have to create unnecessary files where we don’t need them.
Yes, but also no.
If you’re keeping your components focused on a specific task (and you should be), then it stands to reason that the logic is also focused on a single task.
This means that if you wrap up all relevant logic into an inline composable, you’ve wrapped up all — or nearly all — the logic that this component has:
<script setup> // Create an inline composable const useStuff = () => { <all_our_logic> }; // ...only to destructure most of it to use in our template const { value, anotherValue, eventHandler, anotherEventHandler } = useStuff(); </script>
At which point, we might as well write our logic without that unnecessary wrapper:
<script setup> const value = ... const anotherValue = ... const eventHandler = ... const anotherEventHandler = ... </script>
However, if you have do have logic that can be encapsulated nicely within this inline composable, it could make your code cleaner and easier to use.
Using lexical scoping to create more boundaries helps you to understand and think through your code, which is always helpful.
I spent some time quickly building a small demo of an online food menu so we can see if there might be some benefits to inlining composables.
I say I did this quickly so you don't judge the quality of this code — it's good enough to illustrate my point here.
Don't worry too much about trying to understand everything in this example, either. We just need to know how inline composables might work in a larger component.
You can check out the repo here.
First, let’s see the types so we know what we’re working with:
export type Size = { size: string price: number } export type Upgrade = { id: string name: string price: number description: string } export type MenuItem = { id: string name: string description: string cuisine: string sizes: Size[] upgrades: Upgrade[] } export type OrderItem = { menuItem: string size: string upgrades: string[] } export type Order = OrderItem[]
Next, let’s look at the template.
We let the user pick different menu items, their size, any upgrades, and then display the order summary at the bottom of the page:
<template> <div> <h1>Menu</h1> <div v-for="item in menu" :key="item.id"> <h3>{{ item.name }} — ${{ item.sizes[0].price }}</h3> <p>{{ item.description }}</p> <!-- Select button --> <button @click="selectItem(item)">Select</button> <div v-if="currentSelection?.menuItem === item.id"> <!-- Choose the size --> <div> <label>Size:</label> <select v-model="currentSelection.size"> <option v-for="size in item.sizes" :key="size.size" :value="size.size" > {{ size.size }} - ${{ size.price }} </option> </select> </div> <!-- Choose upgrades --> <div> <label>Upgrades:</label> <div v-for="upgrade in item.upgrades" :key="upgrade.id" > <input type="checkbox" v-model="currentSelection.upgrades" :value="upgrade.id" /> {{ upgrade.name }} - ${{ upgrade.price }} </div> </div> <button @click="addToOrder">+ Add to Order</button> </div> </div> <h2>Order Summary</h2> <div class="order-item" v-for="(item, key) in orderSummary" :key="key" > <div> <strong>{{ key }}</strong> </div> <div>Quantity: {{ item.quantity }}</div> <div>Price each: ${{ item.price }}</div> </div> <div>Total: ${{ orderTotal }}</div> </div> </template>
Finally, we get to the logic.
I’ve put it all into an inline useOrder
composable, but since there isn’t any non-order logic, it feels kind of weird:
import { ref, computed } from 'vue' import type { MenuItem, OrderItem, Order } from './types' import menu from './menu' const useOrder = () => { const currentSelection = ref<OrderItem | undefined>(undefined) const order = ref<Order>([]) const findMenuItem = (id: string): MenuItem | undefined => menu.find((item) => item.id === id) const selectItem = (item: MenuItem) => { currentSelection.value = { menuItem: item.id, size: item.sizes[0].size, upgrades: [] } } const addToOrder = () => { if (!currentSelection.value) { return } order.value.push(currentSelection.value) currentSelection.value = undefined } const orderTotal = computed((): number => { let total = 0 order.value.forEach((orderItem) => { const menuItem = findMenuItem(orderItem.menuItem) if (!menuItem) { return } const size = menuItem.sizes.find( ({ size }) => size === orderItem.size ) if (!size) { return } total += size.price orderItem.upgrades.forEach((upgradeId) => { const upgrade = menuItem.upgrades.find( ({ id }) => id === upgradeId ) if (!upgrade) { return } total += upgrade.price }) }) return total }) const orderSummary = computed(() => { const summary: Record< string, { quantity: number; price: number } > = {} order.value.forEach((orderItem) => { const menuItem = findMenuItem(orderItem.menuItem) if (!menuItem) { return } const size = menuItem.sizes.find( ({ size }) => size === orderItem.size ) if (!size) { return } let upgradesString = '' orderItem.upgrades.forEach((upgradeId) => { const upgrade = menuItem.upgrades.find( ({ id }) => id === upgradeId ) if (!upgrade) { return } upgradesString += ` - ${upgrade.name}` }) const key = `${menuItem.name} (${size.size})${upgradesString}` summary[key] = summary[key] ? { quantity: summary[key].quantity + 1, price: size.price } : { quantity: 1, price: size.price } }) return summary }) return { currentSelection, selectItem, addToOrder, orderTotal, orderSummary } } const { currentSelection, selectItem, addToOrder, orderTotal, orderSummary } = useOrder()
There is a single method that gets encapsulated inside of this composable:
const findMenuItem = (id: string): MenuItem | undefined => menu.find((item) => item.id === id)
We don’t return this method since it’s a private utility. So we do get some benefits of inlining here.
It’s very helpful to group the methods that modify state with the state itself, which is a paradigm that dates all the way back to the invention of Object Oriented Programming.
We can achieve this in many different ways: using reactive
, a plain JS object, composables, or using a state management library like Pinia.
Inline composables can be helpful, but only if it helps to create boundaries within your code.
Many components are small enough and focused enough that they don’t really benefit from this. But this technique can help you organize a larger component.