Hook — 05 : 59 a.m., Santo Domingo ↔ Bogotá sprint retro
The Caribbean sunrise backlit my balcony when Camilo in Bogotá sighed over Zoom: “Why does every feature toggle fetch user prefs separately?” We’d copied the same watcher into five components. Coffee in hand, I spiked the duplication by extracting a tinyuseFeatureFlags()
composable, published it to our internal npm registry, and watched bundle size drop while dev velocity soared. Camilo pushed his ticket ahead of schedule—and still made his cycling ride up Monserrate. That win kicks off today’s deep dive into building, packaging, and sharing composable utilities in Vue 3.
Why Composables Are the Secret Sauce
- Single source of truth — centralize side-effects, caching, and utilities.
- Tree-shakable imports — dead code falls out in production.
- Type inference with TS generics keeps IDEs happy.
- Framework-agnostic testing — unit-test logic without rendering components.
- Team velocity — juniors consume a hook instead of copy-pasting snippets.
Anatomy of a Simple Composable
tsCopyEdit// composables/useNow.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useNow(interval = 1000) {
const now = ref(new Date());
let id: number;
onMounted(() => (id = window.setInterval(() => (now.value = new Date()), interval)));
onUnmounted(() => clearInterval(id));
return { now };
}
- Returns reactive state
- Cleans up side-effects
- Accepts config for flexibility
Consuming the Composable
vueCopyEdit<script setup lang="ts">
import { useNow } from '@/composables/useNow';
const { now } = useNow(500);
</script>
<template>
<time :datetime="now.toISOString()">{{ now.toLocaleTimeString() }}</time>
</template>
One line imports the clock anywhere—from dashboards to kiosks.
Building a Fetch-and-Cache Utility
tsCopyEdit// composables/useFetchOnce.ts
import { ref } from 'vue';
const cache = new Map<string, unknown>();
export function useFetchOnce<T = unknown>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
if (cache.has(url)) data.value = cache.get(url) as T;
else {
loading.value = true;
fetch(url)
.then(r => r.json())
.then(j => { cache.set(url, j); data.value = j; })
.catch(e => (error.value = e))
.finally(() => (loading.value = false));
}
return { data, error, loading };
}
Now every component calling /api/settings
shares the same promise and avoids duplicate network chatter—handy for users on prepaid 3 G in Costa Rica.
Remote-Work Insight Box
During a sprint in Panama City, flaky Wi-Fi forced us offline every few minutes. We wrapped Firebase listeners in a useOfflineQueue()
composable that queued writes and replayed them when navigator.onLine
flipped. QA in Medellín thanked us, and product managers never saw a lost update.
Packaging & Publishing Internal Composables
- Create a workspace
packages/vue-utils/
. - Add
package.json
: jsonCopyEdit{ "name": "@acme/vue-utils", "version": "1.0.0", "exports": "./dist/index.js", "types": "./dist/index.d.ts", "peerDependencies": { "vue": "^3.3.0" } }
- Bundle with Vite + Library mode: tsCopyEdit
// vite.config.ts export default defineConfig({ build: { lib: { entry: 'src/index.ts', name: 'VueUtils' } } });
- Publish to GitHub Packages or Verdaccio:
npm publish --access public
. - Consume:
npm i @acme/vue-utils
.
Common Pitfalls & How to Dodge Them
Pitfall | Symptom | Fix |
---|---|---|
Reactive refs leak across apps | Singleton state imported twice | Export factory functions, not singletons |
💥 SSR hydration warnings | Window API used unguarded | Gate in if (process.client) or onMounted |
Bundle bloat | Including entire lodash | Import only needed helpers or use native APIs |
Side-effects not cleaned | Memory leaks on route change | Pair every addEventListener with removal in onUnmounted |
Performance & Accessibility Checkpoints
- DevTools → Component Inspector verifies composable state appears once per component.
- Network tab — ensure cached fetches skip duplicate calls.
- Lighthouse — monitor Total Blocking Time; heavy maths belong in web-workers not composables.
- Unit tests — mock timers with Vitest to guarantee cleanup.
Call-Out Table
Concept / CLI | One-liner purpose |
---|---|
defineStore + composables | Share Pinia logic independent of UI |
provide / inject | Global context fallbacks |
useEventListener | Abstract DOM events with auto-cleanup |
npm version patch | Bump util package semver |
changesets | Automate release notes |
Diagram Description
Components A + B → import useFetchOnce()
→ composable checks Map cache → single fetch → shared reactive ref updates both components.
Wrap-Up
Composable utilities turn scattered snippets into battle-tested building blocks. Keep each hook single-purpose, clean up side-effects, type everything, and ship them via a private registry. Your future self—and every teammate coding from cafés in Santo Domingo, Bogotá, or Florianópolis—will thank you.
Drop your favorite composable patterns or questions below; I’ll respond between flights and late-night arepa sessions.