Composables: reusable logic
A composable is a plain JavaScript function that uses ref, computed, watch and hooks and returns ready-made state with functions. Components split the interface into parts, composables split the logic. By convention a composable's name starts with use.
Where to keep them
Composables go into the src/composables/ folder. The starter project does not have it — create it and the file src/composables/useTodos.js:
// src/composables/useTodos.js
import { ref, computed } from 'vue'
export function useTodos() {
const todos = ref([])
let nextId = 1
const remaining = computed(() => todos.value.filter((t) => !t.done).length)
function addTodo(text) {
todos.value.push({ id: nextId++, text, done: false })
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id)
}
return { todos, remaining, addTodo, removeTodo }
}This is all the task list logic from the previous lessons, just without a template. The function returns an object with a ref, a computed property and two functions.
Using it in a component
<!-- src/App.vue -->
<script setup>
import { useTodos } from './composables/useTodos.js'
import TodoForm from './components/TodoForm.vue'
import TodoItem from './components/TodoItem.vue'
const { todos, remaining, addTodo, removeTodo } = useTodos()
</script>
<template>
<TodoForm @add="addTodo" />
<ul>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@remove="removeTodo(todo.id)"
/>
</ul>
<p>Left: {{ remaining }}</p>
</template>The list works as before, but App.vue is shorter: it only connects the composable to the components. Destructuring is safe here — todos and remaining stay ref objects.
A composable with hooks
Lifecycle hooks can be called inside a composable — they attach to the component that called the function. Create src/composables/useNow.js:
// src/composables/useNow.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useNow() {
const now = ref(new Date())
let timerId
onMounted(() => {
timerId = setInterval(() => (now.value = new Date()), 1000)
})
onUnmounted(() => clearInterval(timerId))
return now
}<!-- src/components/TodoClock.vue -->
<script setup>
import { useNow } from '../composables/useNow.js'
const now = useNow()
</script>
<template>
<p>The time is {{ now.toLocaleTimeString() }}</p>
</template>The clock component from the hooks lesson shrank to three lines of script, and the timer still stops when the component is removed.
Own state or shared
Every call of useTodos() creates a new ref, so two components get two different lists. If you need one list for the whole app, declare the ref outside the function, at module level, or use a Pinia store — it has its own chapter.
Common mistake
Calling a composable not in <script setup> but inside a handler or setTimeout, for example function onClick() { const now = useNow() }. The hooks inside the composable cannot find a component to attach to, Vue prints a warning, and the timer never stops. Call composables synchronously at the top level of <script setup>.