Vue 3 Composables Pattern
Extract and reuse stateful logic with Vue 3 Composables using the Composition API.
Overview
Vue 3 Composables represent the recommended pattern for extracting and reusing stateful logic in Vue applications. They are functions that use Vue Composition API primitives to encapsulate and reuse logic with reactive state. Unlike mixins in Vue 2, Composables provide explicit dependencies, no naming collisions, and full TypeScript support. A composable typically returns a reactive object containing state and functions. By convention, composable function names start with 'use' (useAuth, useFetch, useLocalStorage), making them easily identifiable as composition logic. They can be called from setup() or <script setup> blocks and participate in Vue's reactivity system fully. The lifecycle of state within composables mirrors the component lifecycle when used correctly. Using watchEffect within a composable ensures cleanup when the component unmounts. The onUnmounted hook can register cleanup functions. Composables should be careful about creating memory leaks through event listeners or intervals that aren't properly cleaned up. Composables can composed together to build complex logic from simpler pieces. useAuth might internally use useLocalStorage and useFetch. This composition enables building rich features from focused, testable building blocks. The resulting code is easier to understand than equivalent logic scattered across components.
Code Example
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { useLocalStorage } from './useLocalStorage';
import { useFetch } from './useFetch';
export function useAuth() {
const { set: setToken, remove: removeToken, get: getToken } = useLocalStorage('auth_token');
const { data: user, execute: fetchUser, loading, error } = useFetch('/api/auth/me');
const token = ref(getToken() || null);
const isAuthenticated = computed(() => !!token.value);
const isLoading = ref(false);
const login = async (email: string, password: string) => {
isLoading.value = true;
error.value = null;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const { access_token } = await response.json();
token.value = access_token;
setToken(access_token);
await fetchUser();
return true;
} catch (e) {
error.value = e instanceof Error ? e.message : 'Login failed';
return false;
} finally {
isLoading.value = false;
}
};
const logout = () => {
token.value = null;
removeToken();
user.value = null;
};
return {
token,
user,
isAuthenticated,
isLoading,
error,
login,
logout,
fetchUser,
};
}
export function useLocalStorage(key: string, defaultValue?: any) {
const storedValue = localStorage.getItem(key);
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
watch(
data,
(newValue) => {
if (newValue === null || newValue === undefined) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
},
{ deep: true }
);
const set = (value: any) => {
data.value = value;
};
const get = () => data.value;
const remove = () => {
data.value = null;
localStorage.removeItem(key);
};
return { data, set, get, remove };
}
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
let abortController: AbortController | null = null;
const execute = async (options?: RequestInit) => {
abortController?.abort();
abortController = new AbortController();
loading.value = true;
error.value = null;
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
data.value = await response.json();
return data.value;
} catch (e) {
if (e instanceof Error && e.name !== 'AbortError') {
error.value = e;
}
} finally {
loading.value = false;
}
};
onUnmounted(() => {
abortController?.abort();
});
return { data, error, loading, execute };
}
export function useDebounce<T>(value: T, delay: number) {
const debouncedValue = ref(value) as Ref<T>;
let timeout: ReturnType<typeof setTimeout>;
watch(
() => value,
(newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
debouncedValue.value = newValue;
}, delay);
}
);
onUnmounted(() => {
clearTimeout(timeout);
});
return debouncedValue;
}More Vue.js Rules
Vue 3 Composition API Patterns
Prefer Composition API with script setup for better TypeScript support and reusable logic patterns.
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { useTodos } f...Vue 3 Pinia State Management
Implement centralized state management with Pinia for Vue 3 applications.
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import type { User, Project } from '@/types';
import { userApi } from '@/api...Vue 3 Routing with Vue Router 4
Implement navigation guards, lazy loading, and advanced routing patterns in Vue 3.
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router';
import { computed, onMounted } from 'vue';
import { useAuthStore } from '@/...