YouTip LogoYouTip

Vue3 Blog Composable

\\n\\nComposable β€” Encapsulating Reusable Logic | Rookie Tutorial\\n\\n\\n\\n

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\\n

As 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\\n

Composable extracts reusable logic into independent JS functions, which can be called wherever a component needs them.

\\n\\n

Its essence is a regular function that returns reactive data and methods.

\\n\\n
\\n\\n

Why Do We Need Composables?

\\n\\n

Compare the code structure before and after the change:

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
Without Composable (Before)With Composable (After)
Each component writes its own fetch logicOne usePosts() reused everywhere
Data loading and UI logic mixed togetherData logic is independent, components only care about UI
Changing one logic requires modifying multiple filesChange one place, all components update simultaneously
Component code expands rapidly with business growthComponent code has single responsibility, size is controllable
\\n\\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
ConventionDescriptionExample
Prefix with useInstantly recognizable as a ComposableusePosts, useDarkMode
Place in composables/ directoryEasy to find and maintainsrc/composables/usePosts.js
Return reactive dataReturn ref or reactive, caller remains reactivereturn { articles, isLoading }
One function does one thingSingle responsibility, easy to composeusePosts handles data only, useDarkMode handles theme only
\\n\\n
\\n\\n

usePosts β€” Encapsulating Article Data Logic

\\n\\n

This Composable handles: loading data + category filtering + search filtering.

\\n\\n

Example

\\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\\n

Now the home page component becomes very clean, only needing to call usePosts:

\\n\\n

Example

\\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\\n

The detail page can also reuse usePosts, without rewriting fetch. Note that you need to separately import computed from Vue:

\\n\\n

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

Note: 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
\\n\\n

useDarkMode β€” One Line of Code to Toggle Dark Mode

\\n\\n

The core idea of dark mode: add a class to the html tag, then use CSS variables to control colors.

\\n\\n

The user's choice is stored in localStorage, automatically restored on next visit.

\\n\\n

Example

\\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\\n

Using it in NavBar only takes two lines:

\\n\\n

Example

\\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 ? '&#x2600; Light' : '☾ Dark' }}\\n\\n</button>\\n\\n</nav>\\n\\n</header>\\n\\n</template>
\\n\\n

Coordinating with Global CSS Variables for Theme Switching

\\n\\n

Example

\\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\\n

Now 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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
FeatureMixin (Vue2)Composable (Vue3)
Data sourceOpaque, don't know where properties come fromExplicitly destructured from return, source is clear at a glance
Naming conflictsSame-name properties from multiple Mixins overwrite each otherYou decide variable names, no conflicts exist
Type inferencePoor TypeScript supportNative TypeScript support, automatic type inference
Logic organizationOrganized by options (data/methods/computed)Organized by feature, one composable per file
Logic reuseMixins are hard to compose with each other, prone to implicit dependenciesComposables can call each other, flexible composition
\\n\\n \\n
← Fastapi Blog Jwt AuthFastapi Blog Alembic β†’