Tutorials Logic, IN info@tutorialslogic.com

Vue Composables useFetch, useLocalStorage

Reusable Stateful Logic

A Vue composable is a function that uses Composition API state and lifecycle features to package reusable stateful logic. Its name conventionally begins with use. A composable can return refs, computed values, and actions while each call receives its own state unless shared state is explicitly defined outside the function.

After this lesson, you can extract behavior without extracting presentation, preserve reactivity when consuming results, clean up side effects, and decide when a composable should become a Pinia store.

What are Composables?

Composables are functions that use Vue's Composition API to encapsulate and reuse stateful logic. They are Vue's equivalent of React's custom hooks. By convention, composable function names start with use.

Composables - useFetch, useLocalStorage, useDebounce

Composables - useFetch, useLocalStorage, useDebounce
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
    const data    = ref(null)
    const error   = ref(null)
    const loading = ref(false)

    watchEffect(async () => {
        // Reset state
        data.value  = null
        error.value = null
        loading.value = true

        // toValue() unwraps refs or returns plain values
        const resolvedUrl = toValue(url)
        if (!resolvedUrl) return

        try {
            const res = await fetch(resolvedUrl)
            if (!res.ok) throw new Error(`HTTP ${res.status}`)
            data.value = await res.json()
        } catch (err) {
            error.value = err.message
        } finally {
            loading.value = false
        }
    })

    return { data, error, loading }
}

// Usage in component:
// const { data: users, loading, error } = useFetch('/api/users')
// const { data: post } = useFetch(computed(() => `/api/posts/${postId.value}`))

What are Composables? - JavaScript Example

What are Composables? - JavaScript Example
// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
    const stored = localStorage.getItem(key)
    const value = ref(stored ? JSON.parse(stored) : defaultValue)

    watch(value, (newVal) => {
        if (newVal === null || newVal === undefined) {
            localStorage.removeItem(key)
        } else {
            localStorage.setItem(key, JSON.stringify(newVal))
        }
    }, { deep: true })

    return value
}

// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 500) {
    const debouncedValue = ref(value.value)

    watch(value, (newVal) => {
        const timer = setTimeout(() => {
            debouncedValue.value = newVal
        }, delay)
        return () => clearTimeout(timer)
    })

    return debouncedValue
}

// Usage:
// const theme = useLocalStorage('theme', 'light')
// theme.value = 'dark'  // auto-saves to localStorage

What are Composables? - JavaScript Example 2

What are Composables? - JavaScript Example 2
// composables/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
    const width  = ref(window.innerWidth)
    const height = ref(window.innerHeight)

    function update() {
        width.value  = window.innerWidth
        height.value = window.innerHeight
    }

    onMounted(() => window.addEventListener('resize', update))
    onUnmounted(() => window.removeEventListener('resize', update))

    return { width, height }
}

// composables/useToggle.js
import { ref } from 'vue'

export function useToggle(initial = false) {
    const value = ref(initial)
    const toggle = () => value.value = !value.value
    const setTrue  = () => value.value = true
    const setFalse = () => value.value = false
    return { value, toggle, setTrue, setFalse }
}

// composables/useClipboard.js
import { ref } from 'vue'

export function useClipboard() {
    const copied = ref(false)

    async function copy(text) {
        await navigator.clipboard.writeText(text)
        copied.value = true
        setTimeout(() => copied.value = false, 2000)
    }

    return { copied, copy }
}

// Usage in component:
// <script setup>
// import { useWindowSize } from '@/composables/useWindowSize'
// import { useToggle } from '@/composables/useToggle'
// import { useClipboard } from '@/composables/useClipboard'
//
// const { width, height } = useWindowSize()
// const { value: isOpen, toggle } = useToggle()
// const { copied, copy } = useClipboard()
// </script>

Composable Contract

Design the function around one capability such as pointer position, paginated search, or online status. Accept plain values, refs, or getter functions only when that flexibility helps callers; normalize inputs inside the composable. Return a small named object so the caller can understand which values are state and which functions cause actions.

Returning refs preserves reactivity when callers destructure the result. Destructuring properties from a reactive object can disconnect those local variables from updates unless toRefs is used. A clear ref-based return contract avoids this common surprise.

Effect Cleanup

Side effects must follow component lifetime. Add browser listeners in onMounted and remove the same function in onUnmounted. Abort obsolete fetches when parameters change or the owner unmounts. For watchers, use the cleanup callback to cancel work tied to the previous value so a slow older response cannot overwrite newer state.

Call composables synchronously from setup or <script setup> so Vue can associate lifecycle hooks and watchers with the active component instance. Do not hide composable calls inside arbitrary delayed callbacks.

Store Decision

Use a composable for reusable behavior or state whose ownership is local to each caller. Use Pinia when state represents a shared application domain, requires devtools visibility, or must be coordinated across distant routes and components. A composable may still wrap access to a store when it adds a focused use-case API.

Before you move on

Composable Review

4 checks
  • Give the composable one named capability.
  • Return refs when callers need to destructure reactive results.
  • Remove listeners and cancel obsolete asynchronous work.
  • Keep shared domain state in an intentional store.

Reactivity Leaks

  • Adding an event listener without removing it.

    Register and unregister the same callback with lifecycle hooks.
  • Returning one large reactive object and destructuring it.

    Return refs or convert properties with toRefs.
  • Treating every helper function as a composable.

    Use an ordinary utility when no Vue state or lifecycle feature is needed.

Try this next

Extract One Behavior

0 of 2 completed

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.