Vue.js course Β· Module 6: Components
Composables - Reusable Logic
In this lesson7
Dr. Nova explains: "At NOVA LAB, many modules need the same systems - mission timer, sensor readings, database connection. Instead of copying the same code to every module, we create shared systems. In Vue, such a system is a composable."
Five station panels show the mission time. If each of them has its own copy of the code with setInterval, onMounted and onUnmounted, then fixing a bug in the time calculation means editing five files, and missing one means five different clocks on a single station.
What Is a Composable?
A composable is a function that encapsulates and returns reactive state and logic. It allows sharing logic between multiple components without duplicating code. The Vue documentation describes it as a function that uses the Composition API, meaning ref, computed or lifecycle hooks.
Naming convention: a composable always starts with use - e.g., useTimer, useCounter, useMarsData. The simplest example is a counter made of a container, three functions and an object that returns them:
1// composables/useCounter.js
2import { ref } from 'vue'
3
4export function useCounter(initialValue = 0) {
5 const count = ref(initialValue)
6
7 function increment() {
8 count.value++
9 }
10
11 function decrement() {
12 count.value--
13 }
14
15 function reset() {
16 count.value = initialValue
17 }
18
19 // Return reactive state and functions
20 return { count, increment, decrement, reset }
21}It's an ordinary JavaScript function exported from a .js file - with no template and no styles. It returns a plain object with containers and functions, so a component can unpack it and count stays reactive. The initialValue = 0 parameter has a default value, so useCounter() without an argument starts from zero.
Using a Composable in a Component
In a component you import the function and call it in <script setup>, then unpack the returned values with destructuring:
1<template>
2 <div style="background: #0a0e27; color: #00ff88; padding: 1rem;">
3 <h3>Module counter: {{ count }}</h3>
4 <button @click="increment">+1</button>
5 <button @click="decrement">-1</button>
6 <button @click="reset">Reset</button>
7 </div>
8</template>
9
10<script setup>
11import { useCounter } from './composables/useCounter'
12
13// Destructure returned values
14const { count, increment, decrement, reset } = useCounter(10)
15</script>Each component that uses useCounter() gets its own, independent instance of state. Changing count in one component doesn't affect another. Call composables synchronously, at the top level of <script setup>, and not, for example, inside a click handler - only then does Vue tie them to the component.
File Organization
Composables are stored in a dedicated folder, separate from the components that use them:
1src/
2βββ composables/
3β βββ useTimer.js
4β βββ useCounter.js
5β βββ useFetch.js
6β βββ useMarsData.js
7βββ components/
8β βββ StatusBar.vue
9β βββ ModuleCard.vue
10βββ App.vueMost often it's src/composables/ - Nuxt uses the same folder name. Sometimes you'll come across src/hooks/, a name borrowed from React. Vue doesn't impose anything here; what matters is that the whole project sticks to one convention.
Practical Composable: useTimer
A mission timer is a typical composable used in many station components, because it combines state, a computed value and lifecycle hooks:
1// composables/useTimer.js
2import { ref, computed, onMounted, onUnmounted } from 'vue'
3
4export function useTimer() {
5 const seconds = ref(0)
6 let intervalId = null
7
8 const formatted = computed(() => {
9 const h = String(Math.floor(seconds.value / 3600)).padStart(2, '0')
10 const m = String(Math.floor((seconds.value % 3600) / 60)).padStart(2, '0')
11 const s = String(seconds.value % 60).padStart(2, '0')
12 return `${h}:${m}:${s}`
13 })
14
15 function start() {
16 intervalId = setInterval(() => {
17 seconds.value++
18 }, 1000)
19 }
20
21 function stop() {
22 if (intervalId) {
23 clearInterval(intervalId)
24 intervalId = null
25 }
26 }
27
28 // Automatic lifecycle management
29 onMounted(() => start())
30 onUnmounted(() => stop())
31
32 return { seconds, formatted, start, stop }
33}Hooks called inside a composable attach to the component that called useTimer(), so the clock starts together with it and stops when it is unmounted. The padStart(2, '0') method adds a leading zero, so 7 seconds look like 07. Pay attention to the import: computed has to be brought in from vue just like ref, otherwise the code stops with an error. A component that needs a clock takes from the function only what it needs:
1<!-- Usage in any component -->
2<template>
3 <p>Mission time: {{ formatted }}</p>
4</template>
5
6<script setup>
7import { useTimer } from './composables/useTimer'
8
9const { formatted } = useTimer()
10</script>The component knows nothing about setInterval or cleanup - it gets a ready, formatted time.
Composable: useFetch
Data fetching is another typical pattern: three containers describe the state of the request, and a single function performs it:
1// composables/useFetch.js
2import { ref } from 'vue'
3
4export function useFetch(url) {
5 const data = ref(null)
6 const error = ref(null)
7 const loading = ref(false)
8
9 async function fetchData() {
10 loading.value = true
11 error.value = null
12
13 try {
14 const response = await fetch(url)
15 data.value = await response.json()
16 } catch (err) {
17 error.value = err.message
18 } finally {
19 loading.value = false
20 }
21 }
22
23 return { data, error, loading, fetchData }
24}The loading container is true for the whole duration of the request, error stores the error message, and the finally block turns loading off regardless of the result. Calling useFetch() by itself doesn't fetch anything - only fetchData() does. In the component we call it in onMounted, and the template picks one of three states with the v-if, v-else-if and v-else directives:
1<template>
2 <div>
3 <p v-if="loading">Loading station data...</p>
4 <p v-else-if="error">Error: {{ error }}</p>
5 <ul v-else-if="data">
6 <li v-for="module in data" :key="module.id">{{ module.name }}</li>
7 </ul>
8 </div>
9</template>
10
11<script setup>
12import { onMounted } from 'vue'
13import { useFetch } from './composables/useFetch'
14
15const { data, error, loading, fetchData } = useFetch('/api/modules')
16
17onMounted(() => fetchData())
18</script>The same useFetch will handle the module list, the weather on Mars or a crew report - only the address changes.
Why Composables?
| Without composables | With composables |
|---|---|
| Copy the same timer code to 5 components | One useTimer(), imported everywhere |
| Logic change = editing 5 files | Change in one file |
| Hard testing (logic in components) | Easier testing (logic in separate functions) |
| Components full of repetitive code | Components focused on display |
A composable that uses only ref and computed can be tested by calling it directly in a test, while one with lifecycle hooks needs a host component - we'll come back to this in the Science Laboratory.
Station Construction Plan
At NOVA LAB we build a component application in a fixed order: we plan the component structure, create base components such as a button or a card, extract the logic into composables, assemble the components into views, and finally add lifecycle hooks and styles. I recommend this order to you because each step builds on the previous one. In the editor below, the MarsStation component uses useMarsData and useTimer - open their files and find the patterns from this lesson.
Remember: a composable is a shared station system - a use... function that returns reactive state and logic, and every module connects to it independently.
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsStation from './MarsStation.vue'
4
5const stationName = ref('NOVA LAB Alpha')
6</script>
7
8<template>
9 <div class="composables-lab">
10 <div class="header">
11 <h1>Composables - NOVA LAB</h1>
12 <p class="subtitle">Reusing logic with composables</p>
13 </div>
14
15 <div class="info-panel">
16 <h3>What are composables?</h3>
17 <p>A <strong>composable</strong> is a function starting with <strong>use</strong> (e.g. useMarsData)</p>
18 <p>It encapsulates reactive state (ref) and logic that can be shared between components</p>
19 <p>Structure: export function useXxx() { const state = ref(...); return { state } }</p>
20 </div>
21
22 <MarsStation :station-name="stationName" />
23 </div>
24</template>
25
26<style scoped>
27.composables-lab {
28 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
29 min-height: 100vh;
30 padding: 2rem;
31 color: #ffffff;
32 font-family: 'Courier New', monospace;
33}
34
35.header {
36 text-align: center;
37 margin-bottom: 2rem;
38 padding-bottom: 1rem;
39 border-bottom: 2px solid #00ff88;
40}
41
42.header h1 {
43 font-size: 2rem;
44 color: #00ff88;
45 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
46 margin: 0;
47}
48
49.subtitle {
50 color: #00b4d8;
51 font-size: 1.1rem;
52 margin-top: 0.5rem;
53}
54
55.info-panel {
56 background: rgba(0, 180, 216, 0.1);
57 border: 2px solid #00b4d8;
58 border-radius: 10px;
59 padding: 1.5rem;
60 margin-bottom: 2rem;
61}
62
63.info-panel h3 {
64 color: #00b4d8;
65 margin-top: 0;
66}
67
68.info-panel strong {
69 color: #00ff88;
70}
71</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is a composable in Vue 3?
2. How should a composable be named according to Vue conventions?
These are 2 of 8 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a composable useMarsWeather with reactive data and a fetch function.
- Click in order
Arrange the elements of a composable structure in order:
- Vertical ordering
Arrange the steps for using a composable in a component:
- Code editor
Import and use the composable useMarsWeather in a WeatherPanel.vue component.
- Vertical ordering
Arrange the stages of creating and testing a composable from file to usage:
- Click in order
Arrange the steps from creating to using a composable:
- Code editor
Create a MarsColony with nested HabitatModule, LabModule, and StorageModule components.
- Vertical ordering
Arrange the process from creating to displaying a component:
- Vertical ordering
Arrange the hooks in the order they are called when mounting a component:
- Code editor
Create a set of components: MarsButton and MarsCard with appropriate styles.
- Click in order
Arrange the syntax for using v-bind() in the style section:
- Vertical ordering
Arrange the steps for adding a lifecycle hook to a component:
- Horizontal ordering
Arrange the syntax for using a CSS Module class in the template:
- Code editor
Use the composable useMarsData with onMounted to initialize data in the Dashboard component.
- Vertical ordering
Arrange the stages of building an application from components:
- Horizontal ordering
Arrange the correct import of ref and onMounted from Vue:
- Click in order
Arrange a composable with onMounted:
- Vertical ordering
Arrange the styling methods from widest to narrowest scope:
- Horizontal ordering
Arrange the correct syntax for using onMounted: