Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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>.

Thanks for reading!

Donate to the site — we work hard for you.