In this chapter, you will learn Vue3's best practice β using Composables to encapsulate reusable logic, making your code cleaner and easier to maintain.
\\n\\n\\n\\n
What is a Composable?
\\n\\nAs features grow, the JS logic inside components becomes increasingly lengthy. Both the home page and detail page need to load article data, resulting in duplicated code.
\\n\\nComposable extracts reusable logic into independent JS functions, which can be called wherever a component needs them.
\\n\\nIts essence is a regular function that returns reactive data and methods.
\\n\\n\\n\\n
Why Do We Need Composables?
\\n\\nCompare the code structure before and after the change:
\\n\\n| Without Composable (Before) | \\nWith Composable (After) | \\n
|---|---|
| Each component writes its own fetch logic | \\nOne usePosts() reused everywhere | \\n
| Data loading and UI logic mixed together | \\nData logic is independent, components only care about UI | \\n
| Changing one logic requires modifying multiple files | \\nChange one place, all components update simultaneously | \\n
| Component code expands rapidly with business growth | \\nComponent code has single responsibility, size is controllable | \\n
If you've used Vue2's Mixins, Composable can be understood as a clearer, safer alternative β it has no naming conflicts and data sources are transparent.
\\n\\n\\n\\n
Naming Conventions and Best Practices
\\n\\n| Convention | \\nDescription | \\nExample | \\n
|---|---|---|
Prefix with use | \\nInstantly recognizable as a Composable | \\nusePosts, useDarkMode | \\n
Place in composables/ directory | \\nEasy to find and maintain | \\nsrc/composables/usePosts.js | \\n
| Return reactive data | \\nReturn ref or reactive, caller remains reactive | \\nreturn { articles, isLoading } | \\n
| One function does one thing | \\nSingle responsibility, easy to compose | \\nusePosts handles data only, useDarkMode handles theme only | \\n
\\n\\n
usePosts β Encapsulating Article Data Logic
\\n\\nThis Composable handles: loading data + category filtering + search filtering.
\\n\\nExample
\\n\\n// File path: src/composables/usePosts.js\\n\\nimport{ ref, computed, onMounted } from 'vue'\\n\\nexport function usePosts(){\\n\\nconst articles = ref([])// All article data\\n\\nconst isLoading = ref(true)// Loading state\\n\\nconst error = ref(null)// Error message\\n\\nconst activeCategory = ref('All')// Currently selected category\\n\\nconst keyword = ref('')// Search keyword\\n\\n// Extract all categories (deduplicated)\\n\\nconst categories = computed(()=>{\\n\\nconst cats = articles.value.map(a => a.category)\\n\\nreturn['All', ...new Set(cats)]\\n\\n})\\n\\n// Filtered articles: first by category, then by keyword\\n\\nconst filteredArticles = computed(()=>{\\n\\n let result = articles.value\\n\\n// Filter by category\\n\\nif(activeCategory.value!=='All'){\\n\\n result = result.filter(a => a.category=== activeCategory.value)\\n\\n}\\n\\n// Filter by keyword (matches title or summary)\\n\\nif(keyword.value.trim()){\\n\\nconst kw = keyword.value.trim().toLowerCase()\\n\\n result = result.filter(a =>\\n\\n a.title.toLowerCase().includes(kw)||\\n\\n a.summary.toLowerCase().includes(kw)\\n\\n)\\n\\n}\\n\\nreturn result\\n\\n})\\n\\n// Find single article by ID\\n\\nfunction getArticleById(id){\\n\\nreturn articles.value.find(a => a.id=== id)\\n\\n}\\n\\n// Switch category\\n\\nfunction setCategory(cat){\\n\\n activeCategory.value= cat\\n\\n}\\n\\n// Load data\\n\\n async function fetchPosts(){\\n\\n isLoading.value=true\\n\\n error.value=null\\n\\ntry{\\n\\nconst res = await fetch('/posts.json')\\n\\nif(!res.ok)throw new Error(`HTTP ${res.status}`)\\n\\n articles.value= await res.json()\\n\\n}catch(err){\\n\\n error.value= err.message\\n\\n}finally{\\n\\n isLoading.value=false\\n\\n}\\n\\n}\\n\\n// Auto-load when component mounts\\n\\n onMounted(()=>{\\n\\n fetchPosts()\\n\\n})\\n\\n// Return data and methods for component use\\n\\nreturn{\\n\\n articles,\\n\\n isLoading,\\n\\n error,\\n\\n activeCategory,\\n\\n keyword,\\n\\n categories,\\n\\n filteredArticles,\\n\\n getArticleById,\\n\\n setCategory,\\n\\n fetchPosts\\n\\n}\\n\\n}\\n\\nNow the home page component becomes very clean, only needing to call usePosts:
\\n\\nExample
\\n\\n<!-- File path: src/views/HomeView.vue -->\\n\\n<script setup>\\n\\n import { usePosts } from '../composables/usePosts.js'\\n\\n import BlogCard from '../components/BlogCard.vue'\\n\\n import CategoryFilter from '../components/CategoryFilter.vue'\\n\\n// One line of code to get all article-related data and methods\\n\\n const {\\n\\n isLoading,\\n\\n error,\\n\\n activeCategory,\\n\\n keyword,\\n\\n categories,\\n\\n filteredArticles,\\n\\n setCategory,\\n\\n fetchPosts\\n\\n } = usePosts()\\n\\n</script>\\n\\n<template>\\n\\n<div class="home">\\n\\n<div class="search-bar">\\n\\n<input\\n\\nv-model="keyword"\\n\\ntype="text"\\n\\nplaceholder="Search article titles or summaries..."\\n\\n/>\\n\\n</div>\\n\\n<CategoryFilter\\n\\n:categories="categories"\\n\\n:active-category="activeCategory"\\n\\n@update-category="setCategory"\\n\\n/>\\n\\n<p v-if="isLoading">Loading...</p>\\n\\n<p v-else-if="error">Load failed:{{ error }}\\n\\n<button @click="fetchPosts">Retry</button>\\n\\n</p>\\n\\n<p v-else-if="filteredArticles.length === 0">No matching articles</p>\\n\\n<div v-else class="article-grid">\\n\\n<BlogCard\\n\\nv-for="article in filteredArticles"\\n\\n:key="article.id"\\n\\n:id="article.id"\\n\\n:title="article.title"\\n\\n:summary="article.summary"\\n\\n:date="article.date"\\n\\n:category="article.category"\\n\\n/>\\n\\n</div>\\n\\n</div>\\n\\n</template>\\n\\nThe detail page can also reuse usePosts, without rewriting fetch. Note that you need to separately import computed from Vue:
Example
\\n\\n<!-- File path: src/views/PostView.vue -->\\n\\n<script setup>\\n\\n import { computed } from 'vue'\\n\\n import { useRoute } from 'vue-router'\\n\\n import { usePosts } from '../composables/usePosts.js'\\n\\nconst route = useRoute()\\n\\n const id = Number(route.params.id)\\n\\nconst { isLoading, error, getArticleById } = usePosts()\\n\\n// articles is reactive data, computed will automatically recalculate after fetch completes\\n\\n const article = computed(() => getArticleById(id))\\n\\n</script>\\n\\n<template>\\n\\n<div>\\n\\n<p v-if="isLoading">Loading...</p>\\n\\n<p v-else-if="error">Load failed:{{ error }}</p>\\n\\n<div v-else-if="!article">Post not found</div>\\n\\n<article v-else>\\n\\n<h1>{{ article.title }}</h1>\\n\\n<div v-html="article.content"></div>\\n\\n</article>\\n\\n</div>\\n\\n</template>\\n\\n\\n\\n\\nNote: If the user directly accesses the detail page URL (e.g., refreshing the page), usePosts will re-fetch data. Because the home page and detail page are different component instances, each calling usePosts will execute the fetch in onMounted, and data is not shared. If you need to share the same data across pages, you can lift the state to a Pinia store, or call it once in a top-level component and pass it down via provide / inject.
\\n
\\n\\n
useDarkMode β One Line of Code to Toggle Dark Mode
\\n\\nThe core idea of dark mode: add a class to the html tag, then use CSS variables to control colors.
The user's choice is stored in localStorage, automatically restored on next visit.
\\n\\nExample
\\n\\n// File path: src/composables/useDarkMode.js\\n\\nimport{ ref, watchEffect } from 'vue'\\n\\nexport function useDarkMode(){\\n\\n// Read user's previous setting from localStorage (default to light if none)\\n\\nconst saved = localStorage.getItem('blog-theme')\\n\\nconst isDark = ref(saved ==='dark')\\n\\n// Apply theme to DOM\\n\\nfunction applyTheme(dark){\\n\\nif(dark){\\n\\n document.documentElement.classList.add('dark')\\n\\n localStorage.setItem('blog-theme','dark')\\n\\n}else{\\n\\n document.documentElement.classList.remove('dark')\\n\\n localStorage.setItem('blog-theme','light')\\n\\n}\\n\\n}\\n\\n// watchEffect executes immediately once for initial theme application\\n\\n// After that, whenever isDark changes, automatically sync to DOM\\n\\n watchEffect(()=>{\\n\\n applyTheme(isDark.value)\\n\\n})\\n\\n// Toggle dark mode\\n\\nfunction toggleDark(){\\n\\n isDark.value=!isDark.value\\n\\n}\\n\\nreturn{ isDark, toggleDark }\\n\\n}\\n\\nUsing it in NavBar only takes two lines:
\\n\\nExample
\\n\\n<!-- File path: src/components/NavBar.vue -->\\n\\n<script setup>\\n\\n import { useDarkMode } from '../composables/useDarkMode.js'\\n\\nconst { isDark, toggleDark } = useDarkMode()\\n\\n</script>\\n\\n<template>\\n\\n<header class="navbar">\\n\\n<a href="/"class="logo">TUTORIAL Blog</a>\\n\\n<nav>\\n\\n<a href="/">Home</a>\\n\\n<button class="theme-btn" @click="toggleDark">\\n\\n {{ isDark ? '☀ Light' : 'βΎ Dark' }}\\n\\n</button>\\n\\n</nav>\\n\\n</header>\\n\\n</template>\\n\\nCoordinating with Global CSS Variables for Theme Switching
\\n\\nExample
\\n\\n/* File path: src/assets/main.css */\\n\\n/* Light mode (default) */\\n\\n:root {\\n\\n --bg-primary:#f5f5f5;\\n\\n --bg-card:#ffffff;\\n\\n --text-primary:#333333;\\n\\n --text-secondary:#666666;\\n\\n --border-color:#eeeeee;\\n\\n}\\n\\n/* Dark mode (takes effect when html tag has .dark class) */\\n\\n html.dark{\\n\\n --bg-primary:#1a1a2e;\\n\\n --bg-card:#16213e;\\n\\n --text-primary:#e0e0e0;\\n\\n --text-secondary:#a0a0a0;\\n\\n --border-color:#2a2a4a;\\n\\n}\\n\\n/* Components reference these variables, no component code needs modification when switching themes */\\n\\n body {\\n\\nbackground: var(--bg-primary);\\n\\ncolor: var(--text-primary);\\n\\n}\\n\\n.card{\\n\\nbackground: var(--bg-card);\\n\\nborder:1px solid var(--border-color);\\n\\n}\\n\\nNow when you click the toggle button, the entire site's colors will switch instantly, and your choice will be remembered after refresh.
\\n\\n\\n\\n
Comparison with Vue2 Mixin
\\n\\n| Feature | \\nMixin (Vue2) | \\nComposable (Vue3) | \\n
|---|---|---|
| Data source | \\nOpaque, don't know where properties come from | \\nExplicitly destructured from return, source is clear at a glance | \\n
| Naming conflicts | \\nSame-name properties from multiple Mixins overwrite each other | \\nYou decide variable names, no conflicts exist | \\n
| Type inference | \\nPoor TypeScript support | \\nNative TypeScript support, automatic type inference | \\n
| Logic organization | \\nOrganized by options (data/methods/computed) | \\nOrganized by feature, one composable per file | \\n
| Logic reuse | \\nMixins are hard to compose with each other, prone to implicit dependencies | \\nComposables can call each other, flexible composition | \\n
YouTip