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."
A composable is a function that encapsulates and returns reactive state and logic. It allows sharing logic between multiple components without duplicating code.
Naming convention: a composable always starts with
- e.g., use
useTimer, useCounter, useMarsData.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}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.Composables are stored in a dedicated folder:
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.vueMission timer - a typical composable used in many station components:
1// composables/useTimer.js
2import { ref, 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}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>Data fetching is another typical pattern:
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}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>| 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) | Easy testing (pure functions) |
| Components full of repetitive code | Components focused on display |