๐งฐ Vue.js Composables: Your Magical Toolbox
The Big Idea
Imagine you have a magic toolbox. Inside this toolbox, you keep all your favorite tools - a hammer, a screwdriver, measuring tape. Whenever you need to build something, you just grab the right tool!
Composables in Vue.js are exactly like that magic toolbox. Theyโre reusable pieces of code that you can grab and use in any component. Instead of writing the same code over and over, you write it once and share it everywhere!
๐ฏ What Are Composables?
A composable is a function that uses Vueโs Composition API to package reusable logic.
Think of it this way:
- ๐ช Without composables: Baking the same cookie recipe from scratch every single time
- ๐ง With composables: Having a ready-made cookie dough mix you just use whenever you want cookies!
Simple Example
// Without composable (repeating code)
// In Component A:
const count = ref(0)
const double = computed(() => count.value * 2)
// In Component B (same thing again!):
const count = ref(0)
const double = computed(() => count.value * 2)
// With composable (write once, use everywhere!)
// useCounter.js
export function useCounter() {
const count = ref(0)
const double = computed(() => count.value * 2)
return { count, double }
}
// Now ANY component can use it:
const { count, double } = useCounter()
๐ ๏ธ Creating Composables
Creating a composable is like building your own tool. Hereโs how:
Step 1: Create a New File
Put your composable in the composables/ folder:
src/
composables/
useCounter.js โ Your composable lives here!
Step 2: Write the Function
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
// State
const count = ref(initialValue)
// Computed
const isPositive = computed(() => {
return count.value > 0
})
// Methods
function increment() {
count.value++
}
function decrement() {
count.value--
}
// Return what you want to share
return {
count,
isPositive,
increment,
decrement
}
}
The Recipe ๐ณ
Every composable follows this simple recipe:
graph TD A["1. Import Vue stuff"] --> B["2. Create state with ref/reactive"] B --> C["3. Add computed properties"] C --> D["4. Add methods/functions"] D --> E["5. Return everything you need"]
๐ฎ Using Composables
Using a composable is as easy as calling a function!
In Your Component
<script setup>
import { useCounter } from '@/composables/useCounter'
// Just call the function!
const { count, isPositive, increment, decrement } = useCounter()
// You can even start with a different value
const scores = useCounter(100)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
Real Life Example: Mouse Position
// composables/useMouse.js
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 }
}
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Mouse is at: {{ x }}, {{ y }}</p>
</template>
๐ Composable Conventions
Just like there are rules for driving on the road, there are conventions for composables!
Rule 1: Start with โuseโ
Always name your composable starting with use:
| โ Good Names | โ Bad Names |
|---|---|
useCounter |
counter |
useMouse |
mouseTracker |
useFetch |
fetchData |
useAuth |
authentication |
Rule 2: One File, One Composable
Keep each composable in its own file:
composables/
useCounter.js โ
Good
useMouse.js โ
Good
useAuth.js โ
Good
Rule 3: Call at the Top
Always call composables at the top of setup():
// โ
Good - at the top
const { count } = useCounter()
const { x, y } = useMouse()
// โ Bad - inside conditions or loops
if (something) {
const { count } = useCounter() // Don't do this!
}
Rule 4: Keep It Focused
Each composable should do ONE thing well:
// โ
Good - does one thing
function useCounter() { /* just counting */ }
function useAuth() { /* just authentication */ }
// โ Bad - does too many things
function useEverything() {
/* counting AND auth AND mouse */
}
๐ฆ Composable Return Values
What should your composable give back? Think of it like a gift box!
Pattern 1: Return an Object (Most Common)
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return {
count, // state
increment // method
}
}
// Usage: destructure what you need
const { count, increment } = useCounter()
Pattern 2: Return Reactive Object
import { reactive, toRefs } from 'vue'
export function useUser() {
const state = reactive({
name: '',
email: '',
isLoggedIn: false
})
return toRefs(state)
}
// Usage
const { name, email, isLoggedIn } = useUser()
Pattern 3: Return a Single Value
export function useWindowWidth() {
const width = ref(window.innerWidth)
// ... event listeners
return width // just one value
}
// Usage
const width = useWindowWidth()
What to Include in Return
graph LR A["Return Value Contents"] --> B["๐ฆ State - refs/reactive"] A --> C["๐งฎ Computed - calculated values"] A --> D["๐ฏ Methods - functions to call"] A --> E["๐ง Utilities - helper functions"]
โณ Async Composables
Sometimes you need to fetch data from the internet. Async composables handle this!
The Fetch Pattern
// composables/useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
// Fetch immediately
fetchData()
return {
data,
error,
loading,
refetch: fetchData
}
}
Using Async Composables
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, loading, error, refetch } = useFetch(
'https://api.example.com/users'
)
</script>
<template>
<div v-if="loading">Loading... โณ</div>
<div v-else-if="error">Error: {{ error }} ๐ข</div>
<div v-else>
<ul>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="refetch">Refresh</button>
</div>
</template>
The Three States
Every async operation has three states:
| State | Whatโs Happening |
|---|---|
| โณ Loading | Waiting for data |
| โ Success | Got the data! |
| โ Error | Something went wrong |
๐ Stateful Composables
Stateful composables remember things between uses. Itโs like having a shared notebook everyone can read and write in!
Shared State (Singleton Pattern)
// composables/useSharedCounter.js
import { ref } from 'vue'
// State OUTSIDE the function = shared!
const globalCount = ref(0)
export function useSharedCounter() {
function increment() {
globalCount.value++
}
function decrement() {
globalCount.value--
}
return {
count: globalCount,
increment,
decrement
}
}
How It Works
graph TD A["Component A"] --> C["Shared State"] B["Component B"] --> C D["Component C"] --> C C --> E["globalCount = 5"]
All components see the SAME value!
Independent vs Shared State
// INDEPENDENT - each component gets its own copy
export function useCounter() {
const count = ref(0) // Inside = independent
return { count }
}
// SHARED - all components share one copy
const count = ref(0) // Outside = shared
export function useSharedCounter() {
return { count }
}
| Type | State Location | Use Case |
|---|---|---|
| Independent | Inside function | Each component needs its own data |
| Shared | Outside function | All components need same data |
Real Example: Shopping Cart
// composables/useCart.js
import { ref, computed } from 'vue'
// Shared state - one cart for the whole app!
const items = ref([])
export function useCart() {
const totalItems = computed(() => {
return items.value.length
})
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.price
}, 0)
})
function addItem(item) {
items.value.push(item)
}
function removeItem(id) {
items.value = items.value.filter(i => i.id !== id)
}
function clearCart() {
items.value = []
}
return {
items,
totalItems,
totalPrice,
addItem,
removeItem,
clearCart
}
}
๐ Putting It All Together
Hereโs the complete picture of composables:
graph LR A["Composables"] --> B["Creating"] A --> C["Using"] A --> D["Conventions"] A --> E["Return Values"] A --> F["Async"] A --> G["Stateful"] B --> B1["Function with use prefix"] C --> C1["Call in setup"] D --> D1["Naming rules"] E --> E1["Object with state & methods"] F --> F1["Handle loading/error/data"] G --> G1["Share state across components"]
Quick Reference
| Concept | Remember This |
|---|---|
| Composables | Reusable logic functions |
| Creating | Start with use, return an object |
| Using | Call at top of setup |
| Conventions | use prefix, one file each |
| Return Values | State + computed + methods |
| Async | Handle loading, error, data |
| Stateful | State outside = shared |
๐ You Did It!
You now understand Vue.js Composables! Remember:
- ๐งฐ Composables are your magic toolbox
- โ๏ธ Create them with the
useprefix - ๐ Call them at the top of setup
- ๐ Follow the conventions for clean code
- ๐ฆ Return state, computed, and methods
- โณ Handle async with loading states
- ๐ Use stateful composables for shared data
Now go build amazing things! ๐
